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

文章目录

    • 1.理解沙箱环境
    • 2.沙箱环境接入准备
      • 2.1 访问开发者控制台
      • 2.2 获取重要信息
      • 2.3 处理秘钥
    • 3.接入支付宝支付的流程
    • 4.实现支付
      • 4.1 添加 SDK 依赖
      • 4.2 创建配置类
      • 4.3 支付宝订单管理接口实现流程
      • 4.4 支付宝支付接口实现流程
    • 5.支付宝支付功能演示
    • 7.总结

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 # 支付宝异步通知地址(用户确认支付后,支付宝服务器主动通知商户服务器的异步通知回调,要求改地址允许公网访问)

注意:

  1. 在实际生产中建议将配置项配置在诸如 Nacos 的配置中心,确保数据安全。如果计划直接明文配置在 application.yml 中,至少要进行加解密处理。
  2. 如果你的项目中使用了 Spring Security,则需要将支付宝相关接口的 URI 放入安全白名单中,如 /alipay/**

最后添加支付宝请求客户端配置类 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`                INT UNSIGNED 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`      TINYINT(10) NOT NULL COMMENT '交易状态,见 TradeStatusType',
    `out_trade_no`      VARCHAR(64) DEFAULT NULL COMMENT '商户订单号',
    `pay_method`        TINYINT(10) DEFAULT NULL COMMENT '支付方式,见 PayMethod',
    `product_code`      VARCHAR(32) NOT NULL COMMENT '产品码',
    `product_name`      VARCHAR(256) NOT 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 '用户在交易中支付的金额',
    `status`            TINYINT(1) NOT NULL DEFAULT 0 COMMENT '状态:0 启用,1 禁用,-1 已删除',
    `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`),
    INDEX `IDX_OUT_TRADE_NO` (`out_trade_no`)
) 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 Integer id;

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

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

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

    @ApiModelProperty("交易状态,见 TradeStatusType")
    private Integer tradeStatus;

    @ApiModelProperty("商户订单号")
    private String outTradeNo;

    @ApiModelProperty("支付方式,见 PayMethod")
    private Integer payMethod;

    @ApiModelProperty("产品码")
    private String productCode;

    @ApiModelProperty("产品名称")
    private String productName;

    @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

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

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

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

    /**
     * 支付成功回调
     *
     * @param orderId   订单ID
     * @param payMethod 支付方式
     */
    @Transactional
    Result<?> paySuccess(String orderId, Integer payMethod);

    /**
     * 根据订单ID处理支付成功后的业务逻辑
     *
     * @param orderId   订单ID
     * @param payMethod 支付方式
     */
    @Transactional
    void paySuccessByOrderId(String orderId, Integer payMethod);
}

AlipayOrderServiceImpl:提供了 createOrder()getOrderInfo() 两个方法的具体实现。createOrder()方法中,我们首先生成一个唯一的订单ID,然后模拟商品数量和计算总金额,最终将订单信息插入到数据库中。getOrderInfo()方法则通过订单ID查询数据库,并返回相应的订单实例。除此之外还提供了对应的回调方法在支付成功后执行一定的后续处理逻辑。

TIP:这里采用的是同步回调,有经历的小伙伴可以尝试一下异步回调,用诸如 RabbitMQ 的消息中间件实现回调来进行削峰填谷。

/**
 * <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()));
        // 由于没有实际业务,这里随便设置产品码和产品名称(根据实际业务需求取舍即可)
        alipayOrder.setProductCode("FAST_INSTANT_TRADE_PAY");
        alipayOrder.setProductName("测试产品");
        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("查询订单失败");
    }
  
    @Override
    public Result<?> paySuccess(String orderId, Integer payMethod) {
        AlipayOrder alipayOrder = new AlipayOrder();
        alipayOrder.setOrderId(orderId);
        alipayOrder.setTradeStatus(TradeStatusType.TRADE_SUCCESS.getCode());
        alipayOrder.setPayMethod(payMethod);
        alipayOrder.setGmtPayment(new Timestamp(new Date().getTime()));
        alipayOrderMapper.updateById(alipayOrder);
        // ... 其他业务逻辑(如恢复锁定库存,扣减真实库存等)
        return Result.success("支付成功");
    }

      @Override
      public void paySuccessByOrderId(String orderId, Integer payMethod) {
          QueryWrapper<AlipayOrder> queryWrapper = new QueryWrapper<>();
          queryWrapper.eq("order_id", orderId);
          queryWrapper.eq("trade_status", TradeStatusType.WAIT_BUYER_PAY.getCode());
          queryWrapper.eq("status", StatusType.ENABLE);
          List<AlipayOrder> alipayOrders = alipayOrderMapper.selectList(queryWrapper);
          if (CollectionUtils.isNotEmpty(alipayOrders)) {
              AlipayOrder alipayOrder = alipayOrders.get(0);
              paySuccess(alipayOrder.getOrderId(), payMethod);
          }
      }
}

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 涉及到的启用状态枚举 StatusType

/**
 * 启用状态枚举
 *
 * @author javgo.cn
 * @date 2024/1/7
 */
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public enum StatusType {

    ENABLE(0, "启用"),
    DISABLE(1, "禁用"),
    DELETED(-1, "已删除");

    private final Integer code;
    private final String desc;

    public static StatusType of(Integer code) {
        for (StatusType statusType : StatusType.values()) {
            if (statusType.code.equals(code)) {
                return statusType;
            }
        }
        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());
                alipayOrder.setPayMethod(PayMethod.ALIPAY.getCode());
                log.info("支付宝支付结果通知参数:{}", JSON.toJSONString(alipayOrder));
                QueryWrapper<AlipayOrder> queryWrapper = new QueryWrapper<>();
                queryWrapper.eq("order_id", alipayOrder.getOrderId());
                alipayOrderService.update(alipayOrder, queryWrapper);
                log.info("支付宝订单交易成功,交易状态:{}", tradeStatus);
                // 4.执行回调
                alipayOrderService.paySuccessByOrderId(alipayOrder.getOrderId(), alipayOrder.getPayMethod());
            } 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("支付宝支付查询请求成功");
            // 5.执行回调
            alipayOrderService.paySuccessByOrderId(outTradeNo, PayMethod.ALIPAY.getCode());
            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;
    }
}

AlipayServiceImpl 涉及到的支付方式枚举 PayMethod

/**
 * 支付方式枚举
 *
 * @author javgo.cn
 * @date 2024/1/13
 */
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public enum PayMethod {

    ALIPAY(1, "支付宝"),
    WECHAT(2, "微信"),
    UNIONPAY(3, "银联"),
    APPLEPAY(4, "Apple Pay"),
    CREDITCARD(5, "信用卡"),
    CASH(6, "现金"),
    OTHER(7, "其他");

    private final Integer code;
    private final String description;

    public static PayMethod of(Integer code) {
        for (PayMethod payMethod : values()) {
            if (payMethod.getCode().equals(code)) {
                return payMethod;
            }
        }
        return null;
    }
}

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

/**
 * 支付宝支付控制器
 *
 * @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 即可。

7.总结

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

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


参考资料:

  • 支付宝官方文档:https://opendocs.alipay.com/open/065yhr
  • 电脑网站支付快速接入:https://opendocs.alipay.com/open/270/105899
  • 手机网站支付快速接入:https://opendocs.alipay.com/open/203/105285

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/317420.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

分享一个好用的免费在线扣图网址

具体效果 附地址 https://cutout.aiwave.cc/

【python】基础知识类的语法功能讲解

Python代码定义了一个名为Calculation的类&#xff0c;用于执行基础的数学运算&#xff08;加法、减法、乘法、除法和取模&#xff09;。下面我将详细解释各个部分的功能&#xff0c;并以列表形式总结&#xff1a; 类定义&#xff1a; class Calculation: 定义了一个名为Cal…

iOS Universal Links(通用链接)详细教程

一&#xff1a;Universal Links是用来做什么的&#xff1f; iOS9.0推出的用于应用之间跳转的一种机&#xff0c; 通过一个https的链接启动app。如果手机有安装需要启动的app&#xff0c;可实现无缝跳转。如果没有安装&#xff0c;会打开网页。 实现场景&#xff1a;微信链接无…

Flink窗口(2)—— Window API

目录 窗口分配器 时间窗口 计数窗口 全局窗口 窗口函数 增量聚合函数 全窗口函数&#xff08;full window functions&#xff09; 增量聚合和全窗口函数的结合使用 Window API 主要由两部分构成&#xff1a;窗口分配器&#xff08;Window Assigners&#xff09;和窗口函…

Memcache简介与运维

开源、高性能、高并发的分布式内存缓存系统。 作用 缓存关系型数据库的结果&#xff0c;减少数据库自身访问的次数。 常见内存缓存服务软件对比 memcache 纯内存 redis、memcachedb 可持久化存储&#xff0c;同时会使用磁盘存 …

Typora使用及Markdow学习笔记1

编程如画&#xff0c;我是panda&#xff01; 最近有在学习Markdown&#xff0c;所以这次分享一下我的Markdown学习笔记 目录 前言 一、标题 二、段落 1.换行 2.分割线 三、文字显示 1.字体 2.上下标 四、列表 1.无序列表 2.有序列表 3.任务列表 五、区块 六、代…

外包干了5个月,感觉技术退步明显......

先说一下自己的情况&#xff0c;大专生&#xff0c;18年通过校招进入武汉某软件公司&#xff0c;干了接近4年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落&#xff01; 而我已经在一个企业干了四…

深入理解Spring IOC

1. IOC 理论 IOC 全称控制反转&#xff0c;英文名为 Inversion of Control&#xff0c;它还有一个别名为 DI&#xff08;Dependency Injection&#xff09;,即依赖注入。 在我们刚接触Spring的时候&#xff0c;我们就听说了IOC&#xff0c;但是对于IOC的理解&#xff0c;貌似…

ubuntu 20.04下 Tesla P100加速卡使用

1.系统环境&#xff1a;系统ubuntu 20.04, python 3.8 2.查看cuDNN/CUDA与tensorflow的版本关系如下&#xff1a; Build from source | TensorFlow 从上图可以看出&#xff0c;python3.8 对应的tensorflow/cuDNN/CUDA版本。 3.安装tensorflow #pip3 install tensorflow 新版…

ZooKeeper初探:分布式世界的守护者

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 ZooKeeper初探&#xff1a;分布式世界的守护者 前言Zookeeper的概述分布式系统中的角色和作用&#xff1a; Zookeeper的数据模型Znode的概念和层次结构&#xff1a;Znode的类型和应用场景&#xff1a;…

如何给AI下达精准的指令,哪些提示词对于AI是有效的?

刚上手那会&#xff0c;我倾向于将 prompt 翻译为“指令”&#xff0c;但这并不精确。“指令”通常对应instructions&#xff0c;属于 prompt 中的纯指令部分&#xff0c;通常是一个动宾结构&#xff08;做什么&#xff09;。剩下的部分更多是描述&#xff08;describe&#xf…

【从零开始学习微服务 | 第一篇】什么是微服务

目录 前言&#xff1a; 架构风格&#xff1a; 单体架构&#xff1a; 分布式架构&#xff1a; 微服务&#xff1a; 总结&#xff1a; 前言&#xff1a; 在当今快速发展的软件开发领域&#xff0c;构建大型应用程序已经成为一项巨大的挑战。传统的单体应用架构往往难以满足…

Shiro框架:Shiro内置过滤器源码解析

目录 1. 常见项目中过滤器配置 2.Url访问控制配置解析为内置过滤器 2.1 DefaultFilterChainManager构造并注册内置过滤器 2.2 构造过滤器链 3. Shiro内置过滤器解析 3.1 内置过滤器概览 3.2 公共继承类解析 3.2.1 顶层Filter接口 3.2.2 AbstractFilter 3.2.3 Nameab…

Github上传代码/删除仓库/新建分支的操作流程记录

首先先安装git&#xff0c;安装完git后&#xff0c;看如下操作指令&#xff1a; 输入自己的用户名和邮箱&#xff08;为注册GITHUB账号时的用户名和邮箱&#xff09;&#xff1a; git config --global user.name "HJX-exoskeleton" git config --global user.email …

扫码看图怎么做轮播效果?多组图片用扫码查看的方法

图片通过二维码来做展示现在是很常见的一种方式&#xff0c;用这种方式可以用于多种图片格式。那么当我们需要将图片做成多个分组的轮播图样式展示时&#xff0c;有什么好的方法能够做成这个效果呢&#xff1f;下面就来教大家使用二维码生成器制作图片二维码的操作方法&#xf…

Halcon边缘滤波器edges_image 算子

Halcon边缘滤波器edges_image 算子 基于Sobel滤波器的边缘滤波方法是比较经典的边缘检测方法。除此之外&#xff0c;Halcon也提供了一些新式的边缘滤波器&#xff0c;如edges_image算子。它使用递归实现的滤波器&#xff08;如Deriche、Lanser和Shen&#xff09;检测边缘&…

xtu oj 1475 冰墩墩和冰壶

题目描述 冰壶是被誉为“冰面上的国际象棋”&#xff0c;其计分规则是各自投壶&#xff0c;最后在大本营内&#xff0c;你有几个壶离圆心比对方所有壶离圆心都近就得到几分。 比如红方有两个壶&#xff0c;分别在坐标(1,1),(−2,1)&#xff1b;黄方也有两个壶&#xff0c;分别…

python中的Quene使用方法,包含多线程和多进程

在Python中&#xff0c;队列&#xff08;Queue&#xff09;是一种抽象的数据类型&#xff0c;它遵循先进先出&#xff08;FIFO&#xff09;的原则。队列是一种特殊的线性表&#xff0c;只允许在表的前端&#xff08;front&#xff09;进行删除操作&#xff0c;而在表的后端&…

蓝桥杯省赛无忧 STL 课件16 set

01 set集合 修改set比较方法的常见手段&#xff0c;后面的multiset类似 #include<bits/stdc.h> using namespace std; int main() {set<int,greater<int>> myset;myset.insert(25);myset.insert(17);myset.insert(39);myset.insert(42);for(const auto&…

黑马python就业课

文章目录 初级中级高级初级课程分享 初级 中级 高级 初级课程分享 链接&#xff1a;https://pan.baidu.com/s/1aiJHaThezv_mSI1rnV3d7g 提取码&#xff1a;xdpc