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 # 支付宝应用IDalipayPublicKey: 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 {@Beanpublic 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> {/*** 创建支付宝订单*/@TransactionalResult<?> createOrder();/*** 支付宝回调** @param orderId 订单的唯一标识符*/@TransactionalResult<?> getOrderInfo(String orderId);/*** 支付成功回调** @param orderId   订单ID* @param payMethod 支付方式*/@TransactionalResult<?> paySuccess(String orderId, Integer payMethod);/*** 根据订单ID处理支付成功后的业务逻辑** @param orderId   订单ID* @param payMethod 支付方式*/@Transactionalvoid 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 {@Resourceprivate AlipayOrderMapper alipayOrderMapper;@Overridepublic 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, "创建订单成功");}@Overridepublic 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("查询订单失败");}@Overridepublic 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("支付成功");}@Overridepublic 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 {@Resourceprivate 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 {@Resourceprivate AlipayConfig alipayConfig;@Resourceprivate AlipayClient alipayClient;@Resourceprivate 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";@Overridepublic Result<?> initiatePcPayment(AliPayReq aliPayReq) {return initiatePayment(aliPayReq, PC_PRODUCT_CODE, "支付宝 PC 端支付请求失败", "支付宝 PC 端支付请求成功");}@Overridepublic Result<?> initiateMobilePayment(AliPayReq aliPayReq) {return initiatePayment(aliPayReq, MOBILE_PRODUCT_CODE, "支付宝手机端支付请求失败", "支付宝手机端支付请求成功");}@Overridepublic 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("支付宝支付结果通知处理失败");}@Overridepublic 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.01bizContent.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 {@Resourceprivate AlipayConfig alipayConfig;@Resourceprivate 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")@ResponseBodypublic 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")@ResponseBodypublic 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 {@Beanpublic 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.mzph.cn/news/619276.shtml

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

相关文章

【UEFI基础】EDK网络框架(VLAN)

VLAN VLAN代码综述 在MNP中有很多的VLAN介绍&#xff0c;MNP存在的一个重要原因也是为了处理VLAN&#xff0c;而本文介绍的NetworkPkg\VlanConfigDxe\VlanConfigDxe.inf其实只是一个帮助模块&#xff0c;真正的VLAN配置还是在MNP中。 VLAN同样是一个UEFI Driver Model&#…

Redis实现分布式会话

Redis实现分布式会话 1 什么是分布式会话 1 这是我么之前学过的注册登录模式 2 如果非常多的人访问&#xff0c;因为单台服务器的访问承受能力是有限的&#xff0c;那么我们就想用多态服务器来承担压力 3 一般通过负载均衡的方式来实现&#xff0c;来分担服务器的压力。 4 负…

【PlantUML】- 时序图

写在前面 本篇文章&#xff0c;我们来介绍一下PlantUML的时序图。这个相对类图来讲&#xff0c;比较简单&#xff0c;也不需要布局。读完文章&#xff0c;相信你就能实际操作了。 目录 写在前面一、基本概念二、具体步骤1.环境说明2.元素3.语法4.示例 三、参考资料写在后面系列…

软件测试|Python数据可视化神器——pyecharts教程(十)

使用pyecharts绘制漏斗图 简介 漏斗图&#xff08;Funnel Chart&#xff09;是一种用于可视化数据流程或转化率的图表类型。它通常由一系列阶段组成&#xff0c;每个阶段都有一个名称和一个值&#xff0c;表示在该阶段的转化量或数据流程的进展情况。漏斗图的名称来源于其外观…

Web自动化测试,一定得掌握的 8 个核心知识点

使用 cypress 进行端对端测试&#xff0c;和其他的一些框架有一个显著不同的地方&#xff0c;它使用 javascript 作为编程语言。传统主流的 selenium 框架是支持多语言的&#xff0c;大多数 QA 会的python 和 java 语言都可以编写 selenium 代码&#xff0c;遇到需要编写 js 代…

好用的便签有哪些?windows便签工具在哪打开?

每当我8点准时上班&#xff0c;在等待电脑开机的过程&#xff0c;我都会习惯性地思考整理今天要晚上的任务&#xff0c;列出所要完成的待办事项。随着每一项任务的清晰呈现&#xff0c;我的心情也逐渐明朗起来。当然了&#xff0c;这个时候&#xff0c;我迫切需要一款好用的便签…

VS游戏打包教程

我用得天天酷跑小游戏做的例子 1:安装打包插件 2:在解决方案里新建一个项目 3:新建一个setup项目 4:界面如下(通过右键folder,可以创建folder目录和输出) 5:素材文件 6:素材放完了就项目输出 7:创建快捷方式 右键这个主输出选择第一个create shortcut 8:将这个快捷方式,拖到,…

算法通关村番外篇-LeetCode编程从0到1系列一

大家好我是苏麟 , 今天开始带来LeetCode编程从0到1系列 . 编程基础 0 到 1 , 50 题掌握基础编程能力 大纲 1768.交替合并字符串389. 找不同28. 找出字符串中第一个匹配项的下标242. 有效的字母异位词459. 重复的子字符串283. 移动零66. 加一1822. 数组元素积的符号1502. 判断能…

Canopen学习笔记——sync同步报文增加数据域(同步计数器)

1.Canfestival同步报文sync的设置 在OD表中的配置如下&#xff1a; 如果0x1006索引的同步报文循环周期时间设置为0则禁用同步报文&#xff0c;这里要注意的就是&#xff0c;上面第一张图也提到了&#xff0c;时间单位是us。第二张图&#xff0c;我的0x1006就设置为0xF4240,也就…

Java面试之虚拟机

1、前言 本篇的面试题基于网络整理&#xff0c;和自己编辑。在不断的完善补充哦。 2、什么是虚拟机&#xff1f; Java 虚拟机&#xff0c;是一个可以执行 Java 字节码的虚拟机进程。Java 源文件被编译成能被 Java 虚拟机执行的字节码文件( .class )。 Java 被设计成允许应用程…

LeetCode讲解篇之90. 子集 II

文章目录 题目描述题解思路题解代码 题目描述 题解思路 初始化一个变量start表示当前从哪里开始遍历nums 搜索过程的数字组合加入结果集 从start开始遍历nums 如果当前元素和前一个元素相等&#xff0c;前一个元素没被使用&#xff0c;则触发剪枝去重操作&#xff0c;跳过当…

58.leetcode 最后一个单词的长度

一、题目 二、解答 1. 思路 分2种情况 第一种情况只有一个单词&#xff0c;不包含空格&#xff1a;这种情况直接返回单词本身的长度。第二种情况包含空格&#xff1a;先去掉首尾的空格&#xff0c;根据空格切割字符串生成一个字符串列表&#xff0c;返回倒数第一个索引位置字…

QT周五作业

题目&#xff1a;实现简单水果的价格重量计算 点击一次水果重量1 自动计算总价 代码&#xff1a; widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QListWidgetItem> QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAME…

HarmonyOS@Observed装饰器和@ObjectLink装饰器:嵌套类对象属性变化

Observed装饰器和ObjectLink装饰器&#xff1a;嵌套类对象属性变化 上文所述的装饰器仅能观察到第一层的变化&#xff0c;但是在实际应用开发中&#xff0c;应用会根据开发需要&#xff0c;封装自己的数据模型。对于多层嵌套的情况&#xff0c;比如二维数组&#xff0c;或者数…

每日算法打卡:蚂蚁感冒 day 13

文章目录 原题链接题目描述输入格式输出格式数据范围输入样例1&#xff1a;输出样例1&#xff1a;输入样例2&#xff1a;输出样例2&#xff1a; 题目分析示例代码 原题链接 1211. 蚂蚁感冒 题目难度&#xff1a;简单 题目来源&#xff1a;第五届蓝桥杯省赛C A/B组 题目描述…

echarts 3D地图

vueecharts 3D地图,可自定义地图背景底图。鼠标放上显示弹窗&#xff0c;弹窗自动切换。 <template><div id"gbznt" class"gbznt" ref"gbznt"><img class"mapBg" src"../../../img/propertyTransaction/echart-bg…

Django教程第2章| Web开发实战 |用户管理模块

前言 从第2章开始&#xff0c;我们正式以实战为核心开发用户管理系统&#xff0c;计划实现效果图所有模块功能。 本章我们将开始实现我们第一个功能模块&#xff1a;用户管理。 技术栈 Boostrap、jQuery、​​​Django 功能模块 模块进度功能点部门管理完成增删改查&…

nodejs+vue+ElementUi音乐分享社交网站77l8j

本文介绍的系统主要分为两个部分&#xff1a;一是前台界面&#xff1a;用户通过注册登录可以实现音乐播放、新闻浏览、留言评论等功能&#xff1b;另一个是后台界面&#xff1a;音乐网站管理员对用户信息进行管理&#xff0c;上传更新音乐资源&#xff0c;发布最新音乐资讯等功…

NUS CS1101S:SICP JavaScript 描述:五、使用寄存器机进行计算

原文&#xff1a;5 Computing with Register Machines 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 我的目标是表明天堂机器不是一种神圣的生命体&#xff0c;而是一种钟表&#xff08;相信钟表有灵魂属性的人将制造者的荣耀归功于作品&#xff09;&#xff0c;因为…

debian12部署Gitea服务之二——部署git-lfs

Debian安装gitlfs: 先更新下软件包版本 sudo apt update 安装 sudo apt install git-lfs 验证是否安装成功 git lfs version cd到Gitea仓库目录下 cd /mnt/HuHDD/Git/Gitea/Repo/hu/testrepo.git 执行lfs的初始化命令 git lfs install客户机Windows端在官网下载并安装Git-Lfs 再…