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

news2024/11/10 13:05:08

文章目录

    • 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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

VMware workstation安装debian-12.1.0虚拟机(最小化安装)并配置网络

VMware workstation安装debian-12.1.0虚拟机&#xff08;最小化安装&#xff09;并配置网络 Debian 是一个完全自由的操作系统&#xff01;Debian 有一个由普罗大众组成的社区&#xff01;该文档适用于在VMware workstation平台安装最小化安装debian-12.1.0虚拟机。 1.安装准…

YOLOv5改进 | 注意力篇 | CGAttention实现级联群体注意力机制 (全网首发改进)

一、本文介绍 本文给大家带来的改进机制是实现级联群体注意力机制CascadedGroupAttention,其主要思想为增强输入到注意力头的特征的多样性。与以前的自注意力不同,它为每个头提供不同的输入分割,并跨头级联输出特征。这种方法不仅减少了多头注意力中的计算冗余,而且通过增…

YOLOv5改进 | 二次创新篇 | 结合iRMB和EMA形成全新的iEMA机制(全网独家创新)

一、本文介绍 本文给大家带来的改进机制是二次创新的机制,二次创新是我们发表论文中关键的一环,为什么这么说,从去年的三月份开始对于图像领域的论文发表其实是变难的了,在那之前大家可能搭搭积木的情况下就可以简单的发表一篇论文,但是从去年开始单纯的搭积木其实发表论…

序章 熟悉战场篇—了解vue的基本操作

了解vue 的基本目录&#xff1a; dist 是打包后存放的目录(后续可以改)node_modules 是依赖包public 是静态index页面src 是存放文件的目录assets 是存放静态资源的目录components 是存放组件的目录views 是存放页面文件的目录&#xff08;没有views 自己新建一个&#xff09;A…

【一】通信协议概述

通信协议概述 简介&#xff1a; 很早之前就思考了要写一下电力系统常用的几种通信协议&#xff0c;一直拖着也没有行动&#xff0c;这次终于下定决心来出一个《通信协议》这样的专栏。电力行业数字化方面资料较少&#xff0c;我理解主要一方面是数字化程度还不高&#xff0c;一…

小程序基础学习(js混编)

在组件中使用外部js代码实现数据改变 先创建js文件 编写一些组件代码 编写外部js代码 在组件的js中引入外部js 在 app.json中添加路径规则 组件代码 <!--components/my-behavior/my-behavior.wxml--> <view><view>当前计数为{{count}}</view> <v…

java自动化将用例和截图一起执行测试放入world中直接生成测试报告【搬代码】

1.首先我们得用例写好之后放入文档中&#xff0c;把不用的案例类型、前置条件去掉之后&#xff0c;如图&#xff1a; 放到桌面后&#xff0c;先看执行结果&#xff1a; 直接上代码 package com.znzdh.qitagongju;import jxl.Sheet; import jxl.Workbook; import org.apache…

SpringBoot读取配置文件中的内容

文章目录 1. 读取配置文件application.yml中内容的方法1.1 Environment1.2 Value注解1.3 ConfigurationProperties 注解1.4 PropertySources 注解&#xff0c;获取自定义配置文件中的内容&#xff0c;yml文件需要自行实现适配器1.5 YamlPropertiesFactoryBean 加载 YAML 文件1.…

Java面试基础|数据结构 -实时更新

1.HashMap和ConcurrentHashMap介绍 核心是一个Node数组&#xff0c;数据结构与hashMap相似 使用CAS操作来实现无锁的更新&#xff0c;提高了并发性。当更新节点时&#xff0c;它会使用CAS来替换节点的值或链接&#xff0c;如果CAS失败&#xff0c;表明有其他线程也在进行修改&a…

C语言--单链表的创建及使用详解

C语言--单链表的创建及使用详解 1. 单链表定义1.1 工作原理1.2 优点 2. 单链表的创建2.1 文件创建2.2 节点创建2.3 链表显示 3. 链表操作3.1 尾插3.2 头插3.3 尾删3.4 头删3.5 指定数据寻找3.6 指定位置前插入3.7 指定位置删除 4. 单链表总内容4.1 test.c文件4.2 SList.h文件4.…

强化学习应用(一):基于Q-learning的物流配送路径规划研究(提供Python代码)

一、Q-learning算法简介 Q-learning是一种强化学习算法&#xff0c;用于解决基于马尔可夫决策过程&#xff08;MDP&#xff09;的问题。它通过学习一个值函数来指导智能体在环境中做出决策&#xff0c;以最大化累积奖励。 Q-learning算法的核心思想是使用一个Q值函数来估计每…

ubuntu连接xshell怎么连接

在网上找了好多办法都不行 例如 太久没打开Ubuntu可能输入命令查不到IP地址&#xff0c;解决办法也比较简单&#xff0c;首先第一步 确定自己能不能进入管理员root权限&#xff08;输入命令su&#xff09;&#xff0c;如果没有的话得重新配置&#xff0c;如下图 这是因为当前Ub…

DC-DC变换集成电路芯片B34063——工作电压范围宽,静态电流小

B34063为一单片DC-DC变换集成电路&#xff0c;内含温度补偿的参考电压源(1.25V)、比较器、能有效限制电流及控制工作周期的振荡器,驱动器及大电流输出开关管等&#xff0c;外配少量元件&#xff0c;就能组成升压、降压及电压反转型DC-DC变换器。 主要特点&#xff1a; ● 工作…

文本翻译GUI程序,实现简单的英汉互译

项目地址&#xff1a;mendianyu/txtTranslate: 文本翻译GUI程序&#xff0c;实现简单的英汉互译 (github.com) 文本翻译GUI程序&#xff0c;实现简单的英汉互译 项目结构 三个java文件加一个pom文件 项目运行效果 语言可选择en(英语) zh(汉语) auto(自动识别&#xff0c;仅源语…

推荐算法常见的评估指标

推荐算法评估指标比较复杂&#xff0c;可以分为离线和在线两部分。召回、粗排、精排和重排由于定位区别&#xff0c;其评估指标也会有一定区别&#xff0c;下面详细讲解。 1 召回评价体系 召回结果并不是最终推荐结果&#xff0c;其本质是为后续排序层服务的&#xff0c;故核…

【深度学习目标检测】十五、基于深度学习的口罩检测系统-含GUI和源码(python,yolov8)

YOLOv8是一种物体检测算法&#xff0c;是YOLO系列算法的最新版本。 YOLO&#xff08;You Only Look Once&#xff09;是一种实时物体检测算法&#xff0c;其优势在于快速且准确的检测结果。YOLOv8在之前的版本基础上进行了一系列改进和优化&#xff0c;提高了检测速度和准确性。…

通过IP地址识别风险用户

随着互联网的迅猛发展&#xff0c;网络安全成为企业和个人关注的焦点之一。识别和防范潜在的风险用户是维护网络安全的关键环节之一。IP数据云将探讨通过IP地址识别风险用户的方法和意义。 IP地址的基本概念&#xff1a;IP地址是互联网上设备的独特标识符&#xff0c;它分为IP…

前端工程化相关

工具方法&#xff1a; 知道软件包名&#xff0c;拿到源码或者路径的方法 在浏览器输入以下内容&#xff0c;就可以找到你想要的。。。 unpkg.com/输入包名 一、模块化 ESM特性清单&#xff1a; 自动采取严格模式&#xff0c;忽略“use strict”每个ESM模块都是单独的私有作用…

“华为杯“第四届中国研究生数学建模竞赛-D题:邮路规划与邮车调度

目录 摘 要&#xff1a; 1.问题的重述 2.模型的假设与符号说明 2.1 针对本问题&#xff0c;本文做出如下假设 2.2 符号说明 3.问题的数学模型 4.问题的求解 4.1 问题一的求解 4.1.1 最少邮车数的求法 4.1.2 邮路规划及路径选择 4.1.3 问题的求解结果 4.2 问题二的求…

FPGA之初探

FPGA的构成 基本逻辑单元CLB CLB是FPGA的基本逻辑单元&#xff0c; 一个 CLB 包括了 2 个 Slices&#xff0c;所以知道Slices的数量就可以知道FPGA的“大概”逻辑资源容量了。一个 Slice 等于 4 个6输入LUT8个触发器(flip-flop)算数运算逻辑&#xff0c;每个 Slice 的 4 个触发…