尚硅谷_尚品甄选_第8章_支付.md 40 KB

[TOC]

库存&支付

1 支付宝支付

1.1 需求说明

订单支付如图所示:

pay

在支付页面点击确认支付按钮此时就需要对接第三方支付系统,给用户展示出第三方支付系统的收银台。

查看接口文档:

支付接口地址及返回结果:

get /payment/alipay/submitAlipay/{orderNo}
返回结果:
支付宝支付H5表单

1.2 支付宝支付

官网地址:https://open.alipay.com/

支付宝(中国)网络技术有限公司 [1] 是国内的第三方支付平台,致力于提供“简单、安全、快速”的支付解决方案 [2] 。支付宝公司从2004年建立开始,始终以“信任”作为产品和服务的核心。旗下有“支付宝”与“支付宝钱包”两个独立品牌。自2014年第二季度开始成为当前全球最大的移动支付厂商。

1.2.1 产品介绍

1 产品特色

选择手机网站支付:https://open.alipay.com/api/detail?code=I1080300001000041949

image-20230709160020998

手机网站支付是指商家在移动端网页展示商品或服务,用户在商家页面确认使用支付宝支付后,浏览器自动跳转支付宝 App 或支付宝网页完成付款的

支付产品。该产品在签约完成后,需要技术集成方可使用。

2 使用示例

image

3 申请条件

支持的账号类型:支付宝企业账号支付宝个人账号

签约申请提交材料要求:

  • 提供网站地址,网站能正常访问且页面显示完整,网站需要明确经营内容且有完整的商品信息。
  • 网站必须通过 ICP 备案,且备案主体需与支付宝账号主体一致。若网站备案主体与当前账号主体不同时需上传授权函。
  • 个人账号申请,需提供营业执照,且支付宝账号名称需与营业执照主体一致。

注意:需按照要求提交材料,若部分材料不合格,收款额度将受到限制(单笔收款 ≤ 2000 元,单日收款 ≤ 20000 元)。若签约时未能提供相关材料

(如营业执照),请在合约生效后的 30 天内补全,否则会影响正常收款。

4 费率
收费模式 费率
单笔收费 0.6%-1.0%

特殊行业费率为 1.0%,非特殊行业费率为 0.6%。特殊行业包含:休闲游戏、网络游戏点卡、游戏渠道代理、游戏系统商、网游周边服务、交易平台、网游运营商(含网页游戏)等。

1.2.2 接入准备

官方文档:https://opendocs.alipay.com/open/203/107084?pathHash=a33de091

整体流程:

image-20230709170008020

为了提供数据传输的安全性,在进行传输的时候需要对数据进行加密:

常见的加密方式:

1、不可逆加密:只能对数据进行加密不能解密

2、可逆加密:可以对数据加密也可以解密

可逆加密可以再细分为:

1、对称加密: 加密和解密使用同一个秘钥

image-20230709170626255

2、非对称加密:加密和解密使用的是不同的秘钥

  • 私钥加密公钥解密:身份认证
  • 公钥加密私钥解密:加密

image-20230709170703058

支付宝为了提供数据传输的安全性使用了两个秘钥对:

image-20230709170850260

沙箱环境使用步骤:

1、注册登录支付宝开放平台

2、进入支付宝开放平台控制台,选择沙箱环境

image-20230709165323586

3、沙箱环境提供的应用已经绑定了相关产品

4、配置应用公钥:

image-20230130234642091

需要下载支付宝秘钥生成器【https://opendocs.alipay.com/common/02khjo】,然后生成秘钥。

注意:使用沙箱买家账号进行测试,提前充值。

1.2.3 手机网站支付快速接入

官方文档:https://opendocs.alipay.com/open/203/105285?pathHash=ada1de5b

系统交互流程图:

image-20230709164753985

作为我们的项目来讲只需要将支付宝的收银台展示给用户即可,后续支付的动作和我们的系统就没有关系了。支付成功以后,支付宝开放平台会请求我

们系统的接口通知支付结果,我们的系统也可以调用支付宝交易查询接口获取支付结果。

1.2.4 官方demo

1、官方demo下载地址:https://opendocs.alipay.com/open/203/105910?pathHash=1a2e3a94

2、将访问demo的eclipse项目更改为idea的maven项目(jdk8)

3、在AlipayConfig类中填写参数信息

4、启动项目进行测试

1.3 搭建支付模块

1.3.1 新建模块

在spzx-modules模块下新建子模块spzx-payment

1.3.2 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>com.spzx</groupId>
        <artifactId>spzx-modules</artifactId>
        <version>3.6.3</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>spzx-payment</artifactId>

    <description>
        spzx-payment支付模块
    </description>

    <dependencies>
        <!-- SpringCloud Alibaba Nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!-- SpringCloud Alibaba Nacos Config -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>

        <!-- SpringCloud Alibaba Sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>

        <!-- SpringBoot Actuator -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!-- Mysql Connector -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>

        <!-- spzx Common DataScope -->
        <dependency>
            <groupId>com.spzx</groupId>
            <artifactId>spzx-common-datascope</artifactId>
        </dependency>

        <!-- spzx Common Log -->
        <dependency>
            <groupId>com.spzx</groupId>
            <artifactId>spzx-common-log</artifactId>
        </dependency>

    </dependencies>

    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

1.3.3 banner.txt

在resources目录下新建banner.txt

Spring Boot Version: ${spring-boot.version}
Spring Application Name: ${spring.application.name}
                            _                           _                    
                           (_)                         | |                   
 _ __  _   _   ___   _   _  _  ______  ___  _   _  ___ | |_   ___  _ __ ___  
| '__|| | | | / _ \ | | | || ||______|/ __|| | | |/ __|| __| / _ \| '_ ` _ \ 
| |   | |_| || (_) || |_| || |        \__ \| |_| |\__ \| |_ |  __/| | | | | |
|_|    \__,_| \___/  \__, ||_|        |___/ \__, ||___/ \__| \___||_| |_| |_|
                      __/ |                  __/ |                           
                     |___/                  |___/                            

1.3.4 bootstrap.yml

在resources目录下新建bootstrap.yml

# Tomcat
server:
  port: 9210

# Spring
spring:
  application:
    # 应用名称
    name: spzx-payment
  profiles:
    # 环境配置
    active: dev
  main:
    allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册
  cloud:
    nacos:
      discovery:
        # 服务注册地址
        server-addr: 192.168.100.131:8848
      config:
        # 配置中心地址
        server-addr: 192.168.100.131:8848
        # 配置文件格式
        file-extension: yml
        # 共享配置
        shared-configs:
          - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}

1.3.5 spzx-payment-dev.yml

在共享配置文件application-dev.yml中新增rabbitMQ信息

spring:
    rabbitmq:
        host: 192.168.200.10
        port: 5672
        username: guest
        password: guest
        publisher-confirm-type: CORRELATED #开启生产者确认机制
        publisher-returns: true #开启生产者退回机制 
        listener:
          simple:
            acknowledge-mode: manual #默认情况下消息消费者是自动确认消息的,如果要手动确认消息则需要修改确认模式为manual
            prefetch: 1 # 消费者每次从队列获取的消息数量。此属性当不设置时为:轮询分发,设置为1为:公平分发

在nacos上添加商品服务配置文件

mybatis-plus:
  mapper-locations: classpath*:mapper/**/*Mapper.xml
  type-aliases-package: com.spzx.**.domain
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 查看日志
# spring配置
spring:
  data:
    redis:
      host: 192.168.200.10
      port: 6379
      password: 
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.200.6:3306/spzx-payment?characterEncoding=utf-8&useSSL=false
    username: root
    password: root
    hikari:
      connection-test-query: SELECT 1
      connection-timeout: 60000
      idle-timeout: 500000
      max-lifetime: 540000
      maximum-pool-size: 10
      minimum-idle: 5
      pool-name: GuliHikariPool

1.3.6 logback.xml

在resources目录下新建logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
    <!-- 日志存放路径 -->
   <property name="log.path" value="logs/spzx-payment" />
   <!-- 日志输出格式 -->
   <property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />

    <!-- 控制台输出 -->
   <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
      <encoder>
         <pattern>${log.pattern}</pattern>
      </encoder>
   </appender>

    <!-- 系统日志输出 -->
   <appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
       <file>${log.path}/info.log</file>
        <!-- 循环政策:基于时间创建日志文件 -->
      <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件名格式 -->
         <fileNamePattern>${log.path}/info.%d{yyyy-MM-dd}.log</fileNamePattern>
         <!-- 日志最大的历史 60天 -->
         <maxHistory>60</maxHistory>
      </rollingPolicy>
      <encoder>
         <pattern>${log.pattern}</pattern>
      </encoder>
      <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!-- 过滤的级别 -->
            <level>INFO</level>
            <!-- 匹配时的操作:接收(记录) -->
            <onMatch>ACCEPT</onMatch>
            <!-- 不匹配时的操作:拒绝(不记录) -->
            <onMismatch>DENY</onMismatch>
        </filter>
   </appender>

    <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
       <file>${log.path}/error.log</file>
        <!-- 循环政策:基于时间创建日志文件 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件名格式 -->
            <fileNamePattern>${log.path}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
         <!-- 日志最大的历史 60天 -->
         <maxHistory>60</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${log.pattern}</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!-- 过滤的级别 -->
            <level>ERROR</level>
         <!-- 匹配时的操作:接收(记录) -->
            <onMatch>ACCEPT</onMatch>
         <!-- 不匹配时的操作:拒绝(不记录) -->
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 系统模块日志级别控制  -->
   <logger name="com.spzx" level="info" />
   <!-- Spring日志级别控制  -->
   <logger name="org.springframework" level="warn" />

   <root level="info">
      <appender-ref ref="console" />
   </root>

   <!--系统操作日志-->
    <root level="info">
        <appender-ref ref="file_info" />
        <appender-ref ref="file_error" />
    </root>
</configuration>

1.3.7 SpzxPaymentApplication

添加启动类

package com.spzx.payment;

/**
 * 购物车模块
 *
 */
@EnableCustomConfig
@EnableRyFeignClients
@SpringBootApplication
public class SpzxPaymentApplication
{
    public static void main(String[] args)
    {
        SpringApplication.run(SpzxPaymentApplication.class, args);
        System.out.println("(♥◠‿◠)ノ゙  系统模块启动成功   ლ(´ڡ`ლ)゙  \n" +
                " .-------.       ____     __        \n" +
                " |  _ _   \\      \\   \\   /  /    \n" +
                " | ( ' )  |       \\  _. /  '       \n" +
                " |(_ o _) /        _( )_ .'         \n" +
                " | (_,_).' __  ___(_ o _)'          \n" +
                " |  |\\ \\  |  ||   |(_,_)'         \n" +
                " |  | \\ `'   /|   `-'  /           \n" +
                " |  |  \\    /  \\      /           \n" +
                " ''-'   `'-'    `-..-'              ");
    }
}

1.3.8 配置网关

在spzx-gateway-dev.yml配置文件中添加会员服务的网关信息

# 支付服务
- id: spzx-payment
  uri: lb://spzx-payment
  predicates:
    - Path=/payment/**
  filters:
    - StripPrefix=1

1.4 搭建订单服务远程接口模块

订单支付需要调用订单服务接口

1.4.1 新建模块

在spzx-api模块下新建子模块spzx-api-order

1.4.2 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>com.spzx</groupId>
        <artifactId>spzx-api</artifactId>
        <version>3.6.3</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>spzx-api-order</artifactId>

    <description>
        spzx-api-order订单接口模块
    </description>

    <dependencies>

        <!-- spzx Common Core-->
        <dependency>
            <groupId>com.spzx</groupId>
            <artifactId>spzx-common-core</artifactId>
        </dependency>

    </dependencies>

</project>

1.4.3 spzx-modules模块引入api依赖

spzx-modules模块引入api依赖

<dependency>
    <groupId>com.spzx</groupId>
    <artifactId>spzx-api-order</artifactId>
    <version>3.6.3</version>
</dependency>

1.5 根据订单号获取订单信息

操作模块:spzx-order

1.5.1 远程调用接口开发

1 OrderInfoController
@Operation(summary = "根据订单号获取订单信息")
@InnerAuth
@GetMapping("/getByOrderNo/{orderNo}")
public R<OrderInfo> getByOrderNo(@PathVariable String orderNo) {
    OrderInfo orderInfo = orderInfoService.getByOrderNo(orderNo);
    return R.ok(orderInfo);
}
2 IOrderInfoService
OrderInfo getByOrderNo(String orderNo);
3 OrderInfoServiceI
@Override
public OrderInfo getByOrderNo(String orderNo) {
    //订单
    OrderInfo orderInfo = baseMapper.selectOne(
        new LambdaQueryWrapper<OrderInfo>().eq(OrderInfo::getOrderNo, orderNo)
    );
    //订单详情
    List<OrderItem> orderItemList = orderItemMapper.selectList(
        new LambdaQueryWrapper<OrderItem>().eq(OrderItem::getOrderId, orderInfo.getId())
    );
    orderInfo.setOrderItemList(orderItemList);
    return orderInfo;
}

1.5.2 openFeign接口定义

操作模块:spzx-api-order

**注意:先将spzx-order模块中的OrderInfo类和OrderItem类移动到spzx-api-order 模块 **

1 RemoteOrderInfoService
package com.spzx.order.api;

@FeignClient(
    contextId = "remoteUserInfoService", 
    value = ServiceNameConstants.ORDER_SERVICE, 
    fallbackFactory = RemoteOrderInfoFallbackFactory.class
)
public interface RemoteOrderInfoService{

    @GetMapping("/orderInfo/getByOrderNo/{orderNo}")
    public R<OrderInfo> getByOrderNo(
        @PathVariable("orderNo") String orderNo, 
        @RequestHeader(SecurityConstants.FROM_SOURCE) String source
    );
}
2 ServiceNameConstants
 /**
  * 订单服务的serviceid
*/
public static final String ORDER_SERVICE = "spzx-order";
3 RemoteOrderInfoFallbackFactory
package com.spzx.order.api.factory;

@Slf4j
public class RemoteOrderInfoFallbackFactory implements FallbackFactory<RemoteOrderInfoService>
{
    @Override
    public RemoteOrderInfoService create(Throwable throwable)
    {
        log.error("订单服务调用失败:{}", throwable.getMessage());
        return new RemoteOrderInfoService()
        {

            @Override
            public R<OrderInfo> getByOrderNo(String orderNo, String source) {
                return R.fail("根据订单号获取订单信息失败:" + throwable.getMessage());
            }
        };
    }
}
4 加载配置类

resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

com.spzx.order.api.factory.RemoteOrderInfoFallbackFactory

1.6 代码生成

生成代码

1.7 支付接口

操作模块:spzx-payment

1.7.1 引入依赖

<dependency>
    <groupId>com.alipay.sdk</groupId>
    <artifactId>alipay-sdk-java</artifactId>
</dependency>

1.7.2 spzx-payment-dev.yml

在nacos上添加支付宝所需要的参数

正式服务器版本:

alipay:
  alipay_url: https://openapi-sandbox.dl.alipaydev.com/gateway.do
  app_id: 9021000123601169
  app_private_key: MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCW770BcTA49wE/YTTyCRpms01xycGmaH/LrCulrn7BeEJdS6xhUDVmpYF3kSdaivZTfPE3vbA/K7BBcWCp3/ouVlIRp73h+pujwNQBx6OYxgoZcxmGLDuu4vZcdQxTq1bwmPmwiDaajA6Qwy4bYVrsJead6Oup/VcrvQ7PxZXYNsCkEe2rCh01RxPErvcic1CNuCPHtJBtEQOIatTjyC9avuywkH6PwDwTSuK+lt0MOlVMYYdfeWwndke4WqDpZzNYrsubrpOi+cVxpSZT+UNZgOBW9n/8QmUb0MmFtgGe295YzbV358j0/gaYbvBl2nmEFTACoO8x6IlFFfJ6v7GPAgMBAAECggEACncU48S1Uz3NGFflE+MNsZE9DWavxpn4Qmbtn885erCb6VwV16l4CmXvVBr8X4rDCYbgMr00Oy0MS31Q9nZRImVZy1+WCtUVdYUpLfZpoW4ZxSn12xe2C6ow6rgy4po+rO/uYA8RuiV73Q2rZLqbU0RNeSVtMZwfLyksnbtVaGhIMNxvbPWf46NFRiq0zWFeW4MtGd+EdsvTyUg5pW+kvcnbd/6vPICpcrgYERxXg4PEVFm/F3MgDvmD8VhPGMERT7ojWIwLSNaYrlfALQFOOTsftg8mzf7iBB39BT3wXjJ0hwPQa/fa8L7Gf8ZDXotZX9/cphmAJZh5IwKSzWOAAQKBgQDEoCuhR8s7oMXG+yjUZmH4zd27UQFbgfi/ROMR1BM9hC9Md9VrpimgfSq4sTKdaMUs+Kx2HZ+qexvDINuR/kBjssFqNopP6ocr1NqpIkNoJvdapfPDwNvr5owCV11jGMDHtPYstCjR9nAQjkvlB+UBmkuPIP9XTSlxxEqjtrOZAQKBgQDEg6OT+JhMU2z3hetgeerShEYXgM5Xt7K2dG5bh/jg2yWjuQ9mHPltbJLv1/XKdqcxyP+wvTf7Pr5o8AunMlqGltwUn8R/1v1U08CoMaB9wjxmE1QJ6fP+cpBuqTjA/rxJfJyGEpqE7q7nsuH3Q+PlQLQwxVdTb/qn+6CdMMM6jwKBgQCiZvDiaxwPigEREo1ssYumJtNHTebjH/zGbi1molWp763paOjb2whyyC8rESCPJuQE+vfnLxybgMczacYxVWD1d8WM5rNiwoEW0N4x6Fzof5a6RmuKeZarx2ZI4MP/f4hq6qqRzdQRZ4yz//PrnZOmuTxDYavE/bSHhOtpQRQiAQKBgQCEexKOMSvLWPq/LENweQC8B3yRX768GBWdN4Umo9Y/EL6hT6N3/dSR1n6AvyDu62rv/0eRSR8yfCw15o6Umze4QIACT2tTnAaRZ2gce8hsv8OkNSeUv2pEgcupLuAaDCG3kRCcunH9aEP14/y+i8BZz3Mq5f9nOrjLArJXBoa3NQKBgETSQyLlceqC9m0qZYQBBycS7KGldOV+vTILOLQafWp3OPNc2kuLd7yQspj8WjAJBkhrcqmw6Pf7tt5E6XRTi6+Z47Zc/PIDSixhYRotdkaELxCY5P3gmAtRNUmP/hv9WZJyGygxAUO9mC/CZkAjWg0JM7yOl3f7+n8utIahKHAM
  alipay_public_key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwoLuiTrn/cTUqv147Bzu9jl0heV3P+dlISB4UulkR8NSQ66h+WGROn/Y6FxUNBiz5++mS9wOpYA2fFikueTeaC5s2kNtDnO9Gb6eduX9sOi/cDbu5kaedwzld4Zg4kLYm304EVILAah8U83Go7VngbeV+55RG700mAm+DSd3GVCRXnlUsDilxK+IfzOhvLbMGWSOsHy0K9IwDI+HfHKW6iN5jW5NE+mQMbyUKquPg12EnBkXtnj7i8q4FY1o3+jbUt906tXMKTWPBOG3JwqRv2ndaB71mQB9Ae4wcCdREco5RnnRbg8kMJzkGApDicsr9+TmjB16vJ5+CB+hce7LFwIDAQAB
  return_payment_url: http://localhost:81/#/pages/money/paySuccess
  notify_payment_url: http://zf896e63.natappfree.cc/payment/alipay/callback/notify

沙箱版本

alipay:
  alipay_url: 沙箱支付宝网关地址
  app_id: 沙箱APPID
  app_private_key: 应用私钥
  alipay_public_key: 支付宝公钥
  return_payment_url: http://app前端服务器/#/pages/money/paySuccess
  notify_payment_url: http://内网穿透地址/payment/alipay/callback/notify

1.7.3 AlipayConfig

package com.spzx.payment.config;

import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AlipayConfig {

    @Value("${alipay.alipay_url}")
    private String alipay_url;

    @Value("${alipay.app_private_key}")
    private String app_private_key;

    public final static String format="json";
    public final static String charset="utf-8";
    public final static String sign_type="RSA2";


    public static String return_payment_url;

    public static  String notify_payment_url;

    public static  String alipay_public_key;

    public static String app_id;

    @Value("${alipay.app_id}")
    public void setApp_id(String app_id) {
        AlipayConfig.app_id = app_id;
    }

    @Value("${alipay.alipay_public_key}")
    public   void setAlipay_public_key(String alipay_public_key) {
        AlipayConfig.alipay_public_key = alipay_public_key;
    }

    @Value("${alipay.return_payment_url}")
    public   void setReturn_url(String return_payment_url) {
        AlipayConfig.return_payment_url = return_payment_url;
    }

    @Value("${alipay.notify_payment_url}")
    public   void setNotify_url(String notify_payment_url) {
        AlipayConfig.notify_payment_url = notify_payment_url;
    }

    @Bean
    public AlipayClient alipayClient(){
        AlipayClient alipayClient=new DefaultAlipayClient(alipay_url,app_id,app_private_key,format,charset, alipay_public_key,sign_type );
        return alipayClient;
    }

}

1.7.4 AlipayController

package com.spzx.payment.controller;

@Slf4j
@Controller
@RequestMapping("/alipay")
public class AlipayController extends BaseController {

    @Autowired
    private IAlipayService alipayService;

       /**
     * 对接支付宝返回支付表单用于H5端支付
     * @param orderNo
     * @return
     */
    @Operation(summary = "对接支付宝返回支付表单用于H5端支付")
    @GetMapping("/submitAlipay/{orderNo}")
    public AjaxResult submitAlipay(@PathVariable String orderNo){
        //1.获取支付宝支付表单
        String payForm = alipayService.submitAlipay(orderNo);
        //2.响应支付宝支付表单,用于H5端支付
        return success(payForm);
    }

}

1.7.5 IAlipayService

package com.spzx.payment.service;

public interface IAlipayService{
    String submitAlipay(String orderNo);
}

1.7.6 AlipayServiceImpl

支付宝示例demo:https://opendocs.alipay.com/open/203/105285?pathHash=ada1de5b

package com.spzx.payment.service.impl;

@Service
@Slf4j
public class AlipayServiceImpl implements IAlipayService {

    @Autowired
    private AlipayClient alipayClient;

    @Autowired
    private IPaymentInfoService paymentInfoService;

       /**
     * 对接支付宝返回支付表单用于H5端支付
     *
     * @param orderNo
     * @return
     */
    @Override
    public String submitAlipay(String orderNo) {
        try {
            //1.保存本地交易记录
            PaymentInfo paymentInfo = paymentInfoService.savePaymentInfo(orderNo);
            if (!"0".equals(paymentInfo.getPaymentStatus())) {
                throw new ServiceException("本地交易记录支付状态有误");
            }
            //2.对接支付宝服务端,产生一笔支付宝交易,获取支付宝支付表单
            // 创建请求对象
            AlipayTradeWapPayRequest alipayRequest = new AlipayTradeWapPayRequest();//创建API对应的request
            // 同步回调 应用引导用户去到成功页面
            alipayRequest.setReturnUrl(AlipayConfig.return_payment_url);
            // 异步回调 用户付款成功后,支付宝异步通知商户系统
            alipayRequest.setNotifyUrl(AlipayConfig.notify_payment_url);

            /******必传参数******/
            JSONObject bizContent = new JSONObject();
            //商户订单号,商家自定义,保持唯一性
            bizContent.put("out_trade_no", paymentInfo.getOrderNo());
            //支付金额,最小值0.01元
            //bizContent.put("total_amount", paymentInfo.getAmount());
            bizContent.put("total_amount", paymentInfo.getAmount());

            //订单标题,不可使用特殊符号
            bizContent.put("subject", paymentInfo.getContent());
            /******可选参数******/
            //手机网站支付默认传值QUICK_WAP_WAY
            bizContent.put("product_code", "QUICK_WAP_WAY");

            alipayRequest.setBizContent(bizContent.toString());
            AlipayTradeWapPayResponse response =
                    alipayClient.pageExecute(alipayRequest, "POST");

            return response.getBody(); //调用SDK生成表单;
        } catch (AlipayApiException e) {
            log.error("支付宝支付异常:{}", e);
            throw new RuntimeException(e);
        }
    }
}

1.7.7 IPaymentInfoService

PaymentInfo savePaymentInfo(String orderNo);

1.7.8 PaymentInfoServiceImpl

@Autowired
private RemoteOrderInfoService remoteOrderInfoService;

/**
 * 保存本地交易记录信息
 *
 * @param orderNo
 * @return
 */
@Override
@Transactional(rollbackFor = Exception.class)
public PaymentInfo savePaymentInfo(String orderNo) {
    //1.根据订单编号查询本地记录记录,存在则返回
    PaymentInfo paymentInfo = this.getOne(
            new LambdaQueryWrapper<PaymentInfo>()
                    .eq(PaymentInfo::getOrderNo, orderNo)
    );
    if (paymentInfo != null) {
        return paymentInfo;
    }
    //2.构建本地交易记录对象 paymentInfo
    PaymentInfo paymentInfo1 = new PaymentInfo();
    paymentInfo1.setCreateBy(SecurityContextHolder.getUserName());
    paymentInfo1.setUserId(SecurityContextHolder.getUserId());
    paymentInfo1.setOrderNo(orderNo);
    paymentInfo1.setPayType(2);
    //3.根据订单编号远程调用订单服务获取订单及订单明细信息
    R<OrderInfo> r = remoteOrderInfoService.getByOrderNo(orderNo, SecurityConstants.INNER);
    if (R.FAIL == r.getCode()) {
        throw new RuntimeException("远程调用订单服务失败,原因:" + r.getMsg());
    }
    OrderInfo orderInfo = r.getData();
    //4. 判断订单状态,如果是已支付或者已关闭,则直接返回
    if (orderInfo.getOrderStatus() != 0) {
        throw new RuntimeException("订单状态异常,不能进行支付");
    }
    paymentInfo1.setAmount(orderInfo.getTotalAmount());
    paymentInfo1.setContent(orderInfo.getOrderItemList().get(0).getSkuName());
    paymentInfo1.setPaymentStatus("0");

    //TODO 支付宝端交易编号、回调时间、回调内容 等 用户付款成功,支付宝会回调商户系统再更新三个字段
    //paymentInfo1.setTradeNo();
    //paymentInfo1.setCallbackTime();
    //paymentInfo1.setCallbackContent();

    //4.保存本地交易记录
    this.save(paymentInfo1);
    return paymentInfo1;
}

1.8 支付宝异步回调

操作模块:spzx-payment

1.8.1 AlipayController

/**
 * 支付宝回调:用户付款成功后,支付宝会通知商户系统支付结果
 * @param paramMap
 * @return
 */
@Operation(summary = "用户付款成功后,支付宝会通知商户系统支付结果")
@PostMapping("/callback/notify")
public String paySuccessNotify(@RequestParam Map<String, String> paramMap){
    String result = alipayService.paySuccessNotify(paramMap);
    return result;
}

1.8.2 配置网关

# 不校验白名单
ignore:
  whites:
    ...
    - /payment/alipay/callback/*

1.8.3 内网穿透介绍

当支付成功以后支付宝无法调用本地接口,因为本地接口是位于一个私有IP地址范围内,并且被路由器或防火墙等设备保护起来。这个私有的网络设备无法直接从公共网络访问,该问题的解决可以使用内网穿透技术。

内网穿透:内网穿透(Intranet Port Forwarding)是一种将本地网络中的服务暴露给公共网络访问的技术。

内网穿透通过在公共网络上建立一个中转服务器,使得公共网络上的设备可以通过该中转服务器访问内网中的设备和服务。具体而言,内网穿透技术允

许您在公共网络上使用一个公网IP地址和端口号来映射到内网中的某个设备或服务的私有IP地址和端口号。

image-20230710185519318

常见的内网穿透工具包括natapp、Ngrok、frp、花生壳等。

官网地址:https://natapp.cn/

试用步骤:

1、注册用户

2、购买隧道

image-20230219170135784

3、购买二级域名,绑定隧道

image-20230219170206186

4、下载客户端

image-20230219170337316

5、客户端使用教程:https://natapp.cn/article/nohup

natapp.exe -authtoken=xxxxx

authtoken信息获取:

image-20230219171016742

1.8.4 异步通知说明

官网地址:https://opendocs.alipay.com/open/203/105286?pathHash=022a439c&ref=api

image-20230710180507583

当用户支付成功以后,支付宝系统会调用我们系统的接口通知支付结果

1.8.5 验证签名

操作模块:spzx-payment

pom.xml

添加依赖

<dependency>
    <groupId>com.spzx</groupId>
    <artifactId>spzx-common-rabbit</artifactId>
    <version>3.6.3</version>
</dependency>
AlipayController
/**
 * 支付宝回调:用户付款成功后,支付宝会通知商户系统支付结果
 * @param paramMap
 * @return
 */
@Operation(summary = "用户付款成功后,支付宝会通知商户系统支付结果")
@PostMapping("/callback/notify")
public String paySuccessNotify(@RequestParam Map<String, String> paramMap){
    String result = alipayService.paySuccessNotify(paramMap);
    return result;
}
AlipayService
/**
 * 支付宝回调:用户付款成功后,支付宝会通知商户系统支付结果
 * @param paramMap
 * @return
 */
String paySuccessNotify(Map<String, String> paramMap);
AlipayServiceImpl
/**
 * 支付宝回调:用户付款成功后,支付宝会通知商户系统支付结果
 *
 * @param paramMap
 * @return
 */
@Override
public String paySuccessNotify(Map<String, String> paramMap) {
    try {
        //1.验证签名 避免出现虚假通知 ,确保该接口调用方是支付宝/防止数据被篡改
        log.info("支付宝支付成功回调参数:{}", paramMap);
        boolean flag = AlipaySignature.rsaCheckV1(
                paramMap,
                AlipayConfig.alipay_public_key,
                AlipayConfig.charset,
                AlipayConfig.sign_type
        );
        if (!flag) {
            log.error("支付宝支付成功回调:签名验证失败");
            return "failure";
        }
        //2.基于通知ID进行幂等性处理
        String notifyId = paramMap.get("notify_id");
        String key = "pay:alipay:notifyId:" + notifyId;
        Boolean onece = redisTemplate.opsForValue().setIfAbsent(key, notifyId, 25, TimeUnit.HOURS);
        if (onece) {
            try {
                //3.验证金额、支付状态 -- 确保用户付款成功
                String tradeStatus = paramMap.get("trade_status");
                if ("TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus)) {
                    //4.更新本地交易记录状态:已支付
                    paymentInfoService.updatePaymentStatus(paramMap);
                    return "success";
                }
            } catch (Exception e) {
                redisTemplate.delete(key);
                throw new RuntimeException(e);
            }
        }
    } catch (AlipayApiException e) {
        throw new RuntimeException(e);
    }
    return null;
}
IPaymentInfoService

更新支付信息

/**
     * 用户付款成功后,更新本地交易记录
     * @param paramMap
     */
void updatePaymentStatus(Map<String, String> paramMap);
PaymentInfoServiceImpl
/**
 * 用户付款成功后,更新本地交易记录
 *
 * @param paramMap
 */
@Override
public void updatePaymentStatus(Map<String, String> paramMap) {
    //1.根据订单编号查询本地交易记录 验证:用户实付金额跟商户侧应付金额是否一致
    String orderNo = paramMap.get("out_trade_no");
    PaymentInfo paymentInfo = this.getOne(
            new LambdaQueryWrapper<PaymentInfo>()
                    .eq(PaymentInfo::getOrderNo, orderNo)
    );
    String buyerPayAmount = paramMap.get("buyer_pay_amount");
    if (paymentInfo.getAmount().compareTo(new BigDecimal(buyerPayAmount)) != 0) {
        log.error("该笔订单实付金额与交易记录金额不一致,订单编号:{}", orderNo);
        throw new RuntimeException("该笔订单实付金额与交易记录金额不一致,订单编号:" + orderNo);
    }
    //2.更新本地交易记录:状态:已支付、支付宝订单号、回调时间、回调内容
    paymentInfo.setPaymentStatus("1");
    paymentInfo.setTradeNo(paramMap.get("trade_no"));
    paymentInfo.setCallbackTime(new Date());
    paymentInfo.setCallbackContent(paramMap.toString());
    this.updateById(paymentInfo);

    //3.发送MQ消息通知订单服务修改订单状态
    rabbitService.sendMessage(MqConst.EXCHANGE_PAYMENT_PAY, MqConst.ROUTING_PAYMENT_PAY, orderNo);
    //4.发送MQ消息通知商品服务库存扣减
    rabbitService.sendMessage(MqConst.EXCHANGE_PRODUCT, MqConst.ROUTING_MINUS, orderNo);

}

2 支付成功处理

2.1 更改订单状态

操作模块:spzx-order

订单支付成功后,我们已经更改了订单支付记录状态,接下来我还有更改订单状态,因为他们是不同的微服务模块,所以我们采用消息队列的方式,保证数据最终一致性

2.1.1 OrderReceiver

package com.spzx.order.receiver;

import com.rabbitmq.client.Channel;
import com.spzx.common.core.utils.StringUtils;
import com.spzx.common.rabbit.constant.MqConst;
import com.spzx.order.service.OrderInfoService;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * @author: atguigu
 * @create: 2025-05-10 16:34
 */
@Slf4j
@Component
public class OrderReceiver {

    @Autowired
    private OrderInfoService orderInfoService;

    /**
     * 监听订单支付成功消息,修改订单状态
     *
     * @param orderNo
     * @param message
     * @param channel
     */
    @RabbitListener(bindings = @QueueBinding(
            exchange = @Exchange(value = MqConst.EXCHANGE_PAYMENT_PAY, durable = "true"),
            value = @Queue(MqConst.QUEUE_PAYMENT_PAY),
            key = MqConst.ROUTING_PAYMENT_PAY
    ))
    @SneakyThrows
    public void paymentSuccess(String orderNo, Message message, Channel channel) {
        if (StringUtils.isNotBlank(orderNo)) {
            log.info("[订单服务]监听到订单支付成功消息:{}", orderNo);
            orderInfoService.paymentSuccess(orderNo);
        }
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
}

2.1.2 IOrderInfoService

void paymentSuccess(String orderNo);

2.1.3 OrderInfoServiceImpl

/**
 * 订单支付成功消息,修改订单状态
 *
 * @param orderNo
 */
@Override
public void paymentSuccess(String orderNo) {
    OrderInfo orderInfo = this.getByOrderNo(orderNo);
    if (orderInfo.getOrderStatus().intValue()==0) {
        orderInfo.setOrderStatus(1);
        this.updateById(orderInfo);
    }
}

2.2 扣减商品库存

订单模块除了更改订单支付状态,还要发送消息通知商品服务模块扣减库存

2.2.1 ProductReceiver

/***
 * 监听订单支付成功后扣减订单中商品库存数量
 * @param orderNo
 * @param channel
 * @param message
 * @throws IOException
 */
@SneakyThrows
@RabbitListener(bindings = @QueueBinding(
        exchange = @Exchange(value = MqConst.EXCHANGE_PRODUCT, durable = "true"),
        value = @Queue(MqConst.QUEUE_MINUS),
        key = MqConst.ROUTING_MINUS
))
public void minus(String orderNo, Channel channel, Message message) {
    if (StringUtils.isNotBlank(orderNo)) {
        log.info("[订单服务]监听到订单支付成功,减库存消息:{}", orderNo);
        //1.幂等性处理
        String key = "order:minus:orderNo:" + orderNo;
        Boolean once = redisTemplate.opsForValue().setIfAbsent(key, orderNo, 5, TimeUnit.MINUTES);
        if (!once) {
            return;
        }
        try {
            //2.业务逻辑处理
            skuStockService.minusStock(orderNo);
        } catch (Exception e) {
            redisTemplate.delete(key);
            throw new RuntimeException(e);
        }
        //3.手动应答
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
}

2.3.2 IProductService

/**
 * 从下单时存入Redis缓存锁定记录,进行库存真正扣减
 * @param orderNo
 */
void minusStock(String orderNo);

2.2.3 ProductServiceImpl

/**
 * 从下单时存入Redis缓存锁定记录,进行库存真正扣减
 *
 * @param orderNo
 */
@Override
public void minusStock(String orderNo) {
    String dataKey = "sku:lock:data:" + orderNo;
    List<SkuLockVo> skuLockVoList = (List<SkuLockVo>) redisTemplate.opsForValue().get(dataKey);
    if (CollectionUtils.isEmpty(skuLockVoList)) {
        return;
    }
    //遍历锁定记录列表,进行真正扣减库存
    skuLockVoList.forEach(skuLockVo -> {
        baseMapper.minusStock(skuLockVo.getSkuId(), skuLockVo.getSkuNum());
    });
    redisTemplate.delete(dataKey);
}

2.2.4 SkuStockMapper

void minusStock(@Param("skuId") Long skuId,@Param("skuNum") Integer skuNum);

2.2.5 SkuStockMapper.xml

<update id="minusStock">
    UPDATE sku_stock set lock_num = lock_num - #{skuNum} , total_num = total_num - #{skuNum} where sku_id = #{skuId} and  del_flag = 0;
</update>