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.rhkb.cn/news/237479.html

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

相关文章

网络协议与攻击模拟_04ICMP协议与ICMP重定向

ICMP协议是网络层协议&#xff0c; 利用ICMP协议可以实现网络中监听服务和拒绝服务&#xff0c;如 ICMP重定向的攻击。 一、ICMP基本概念 1、ICMP协议 ICMP是Internet控制报文协议&#xff0c;用于在IP主机、路由器之间传递控制消息&#xff0c;控制消息指网络通不通、主机是…

Git LFS 大文件存储

Git 碰到大文件的困境 Git 是业界流行的分布式版本控制工具&#xff0c;本地仓库与远端仓库同样保存了全量的文件和变更历史&#xff0c;这样让代码协作变得简单和高效。但也正因为如此&#xff0c;Git针对大型文件&#xff08;例如图片、视频或其他二进制文件&#xff09;的版…

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

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

Jetpack Compose -> 声明式UI Modifier

前言 本章主要介绍下 Compose 的声明式 UI 以及初级写法&#xff1b; 什么是声明式UI 传统UI 传统 UI 方式来声明UI <androidx.appcompat.widget.LinearLayoutCompat android:layout_width"match_parent" android:layout_height"match_parent&quo…

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

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

C# 图解教程 第5版 —— 第24章 预处理指令

文章目录 24.1 什么是预处理指令24.2 基本规则24.3 符号指令&#xff08;#define、#undef &#xff09;24.4 条件编译&#xff08;#if、#else、#elif、#endif&#xff09;24.5 条件编译结构24.6 诊断指令&#xff08;#warning、#error&#xff09;24.7 行号指令&#xff08;#li…

【C++入门到精通】智能指针 [ C++入门 ]

阅读导航 引言一、什么是智能指针二、为什么需要智能指针三、内存泄漏1. 什么是内存泄漏&#xff0c;内存泄漏的危害2. 内存泄漏的示例&#xff0c;以及解决方法3. 内存泄漏分类&#xff08;1&#xff09;堆内存泄漏(Heap leak)&#xff08;2&#xff09;系统资源泄漏 4. 如何检…

Sqoop的增量数据加载策略与示例

当使用Apache Sqoop进行数据加载时&#xff0c;增量数据加载策略是一个关键的话题。增量加载可以仅导入发生变化的数据&#xff0c;而不必每次都导入整个数据集&#xff0c;这可以显著提高任务的效率。本文将深入探讨Sqoop的增量数据加载策略&#xff0c;提供详细的示例代码&am…

大语言模型面试问题

自己在看面经中遇到的一些面试题&#xff0c;结合自己和理解进行了一下整理。 transformer中求和与归一化中“求和”是什么意思&#xff1f; 求和的意思就是残差层求和&#xff0c;原本的等式为y H(x)转化为y x H(x)&#xff0c;这样做的目的是防止网络层数的加深而造成的梯…

管理软件供应链中网络安全工具蔓延的三种方法

软件开发组织不断发展&#xff0c;团队成长&#xff0c;项目数量增加。技术堆栈发生变化&#xff0c;技术和管理决策变得更加分散。 在这一演变过程中&#xff0c;该组织的 AppSec 工具组合也在不断增长。在动态组织中&#xff0c;这可能会导致“工具蔓延”。庞大的 AppSec 工…

Java--RSA非对称加密的实现(使用java.security.KeyPair)

文章目录 前言实现步骤测试结果 前言 非对称加密是指使用不同的两个密钥进行加密和解密的一种加密算法&#xff0c;调用方用使用服务方提供的公钥进行加密&#xff0c;服务方使用自己的私钥进行解密。RSA算法是目前使用最广泛的公钥密码算法。Java提供了KeyPairGenerator类要生…

2024年AMC8模拟考试实测流程、注意事项和常见问题

和往年的AMC8比赛一样&#xff0c;在正式比赛的前一周左右会开放两天的模拟考试时间&#xff0c;AMC8的主办方建议所有的参赛选手重视且参加模拟考试&#xff0c;以测试设备、熟悉流程&#xff0c;避免将来正式考试不小心违规&#xff0c;或者设备不给力。 2024年的AMC8模拟考…

OFBiz RCE漏洞复现(CVE-2023-51467)

漏洞名称 Apache OFBiz 鉴权绕过导致命令执行 漏洞描述 Apache OFBiz是一个非常著名的电子商务平台&#xff0c;是一个非常著名的开源项目&#xff0c;提供了创建基于最新J2EE/XML规范和技术标准&#xff0c;构建大中型企业级、跨平台、跨数据库、跨应用服务器的多层、分布式…

五、带登录窗体的demo

做了一个简单的带登录窗体的demo&#xff0c;有用户名和密码不能为空的验证&#xff0c;原理是在main.cpp的主函数入口处&#xff1a; 1、将默认的MainWindow主窗体注释。 2、新建一个formlogin登录窗体&#xff0c;在主函数中先运行登录窗体。 3、在登录窗体中引用MainWind…

Android json功能解析

1. 简介 JAVAScript Object Notation是一种轻量级的数据交换格式具有良好的可读和便于快速编写的特性。业内主流技术为其提供了完整的解决方案&#xff08;有点类似于正则表达式 &#xff0c;获得了当今大部分语言的支持&#xff09;。  JSON采用兼容性很高的文本格式&#xf…

路由器02_静态路由DHCP

一、静态路由 &#xff11;、静态路由特点 由管理员手工配置&#xff0c;是单向的&#xff0c;缺乏灵活性 &#xff12;、默认路由 默认路由是一种比较特殊静态路由&#xff0c;一般用于末节&#xff08;末梢&#xff09;网络&#xff0c;直接指定目标为任何地方 二、静态…

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

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

Casper Network (CSPR)2024 年愿景:通过投资促进增长

Casper Network (CSPR&#xff09;是行业领先的 Layer-1 区块链网络之一&#xff0c;通过推出了一系列值得关注的技术改进和倡议&#xff0c;已经为 2024 年做好了准备。 在过去的一年里&#xff0c;Casper Network (CSPR&#xff09;不断取得里程碑式的进展&#xff0c;例如推…

高性能RPC框架解密

专栏集锦&#xff0c;大佬们可以收藏以备不时之需&#xff1a; Spring Cloud 专栏&#xff1a;http://t.csdnimg.cn/WDmJ9 Python 专栏&#xff1a;http://t.csdnimg.cn/hMwPR Redis 专栏&#xff1a;http://t.csdnimg.cn/Qq0Xc TensorFlow 专栏&#xff1a;http://t.csdni…