Skip to content

支付 ​

1. 支付业务流程 ​

业务场景如下,用户确认订单之后,跳转到支付界面,点击支付宝扫码支付的图标,就可以进行扫码支付了

1. 用户点击扫码支付

请求url:http://localhost/pay/alipay/submit/{orderId}

image-20230324102648455

2. 跳转到支付宝付款页面

image-20230324152933063

3. 用户扫码付款

image-20230324165118538

4. 浏览器跳转到支付成功页面

image-20230324163337306

2. 支付宝支付 ​

2.1 支付宝简介 ​

支付宝是国内的第三方支付平台,致力于提供简单、安全、快速的支付解决方案。自2014年第二季度开始成为当前全球最大的移动支付)厂商。

2.2 支付详细流程 ​

ini
1. 用户点击扫码支付图标,然后由浏览器发起支付请求,发给Gateway服务

2. Gateway收到请求之后把请求路由分发给 service-pay 服务

3. 支付服务接受请求,收到参数OrderId,然后调用订单服务的接口,查询这个订单是否是未支付的状态

4. 支付服务保存一条支付记录到数据库,并且记录状态为《未支付》

5. 支付服务向支付宝发起请求,获取支付页

6. 把支付页交给浏览器渲染,用户在浏览器中看到如下效果

7. 用户扫码或者是 用户输入账号密码确认支付

8. 同步回调:用户支付完成之后,浏览器发起请求,跳转到支付成功页面

9. 异步回调:用户支付完成之后,支付宝服务器发起请求,把支付结果通知给我们的服务,我们的服务收到请求之后,需要做修改订单状态,修改支付状态,扣减库存等操作

image-20230324165332289

2.3 接入支付流程 ​

image-20230324164020243

  1. 首先申请相关的账号

    • 在那里申请呢? 支付宝开放平台/微信开放平台+微信商户平台

    • 申请什么账号呢?申请商户账号,应用ID

    • 申请账号需要什么资料呢?

      需要提交公司的营业执照,法人代表等身份信息,保证公司和业务的合法性

  2. 绑定商户账号和应用ID,并配置接口访问的秘钥以及下载配置商户

  3. 确定需要接入支付的类别

    • 刷脸支付

    • 扫码支付

    • APP支付

    • 手机网站支付

    • 电脑网站支付

  4. 查看你要接入的支付类别相对应的接口文档

  5. 编写代码

2.4 沙箱使用说明 ​

由于我们目前在开发的时候,没有商户账号,也没有营业执照等,所以暂时没有条件在支付宝上申请到对应的账号与AppId,不过支付宝为了协助我们开发者进行开发联调测试,推出了一个沙箱环境。

沙箱环境是协助开发者进行接口开发及主要功能联调的模拟环境,目前仅支持网页/移动应用和小程序两种应用类型。在沙箱完成接口调试后,直接修改必要的配置就可以接入正式环境。

沙箱环境地址

通过沙箱环境,我们需要获取的参数:

  1. APPID
  2. 支付宝网关地址
  3. 应用公钥
  4. 应用私钥
  5. 支付宝公钥

当然,我们也可以使用沙箱平台在线调试我们的代码。

2.5 支付宝接口说明 ​

我们是网站应用,所以接入电脑网站支付。

在本项目中,使用到的接口有:

  1. 支付下单,获取支付页面
  2. 统一收单交易查询
  3. 统一收单交易关闭接口
  4. 异步通知说明

2.6 支付功能测试 ​

2.6.1 内网穿透 ​

  1. 为什么需要内网穿透?

    我们当前的支付服务,网关服务等也是运行在网络上,为什么支付宝访问不到我们的支付服务或者是网关呢?

    image-20230327115055579

  2. 如何解决内网穿透问题?

    image-20230327120739464

  3. 内网穿透工具

    • Natapp 下载地址

    • ![](支付 - 改.assets/新手图文教程.png)

    • ![](支付 - 改.assets/natapp后台.png)

    • ![](支付 - 改.assets/内网穿透配置.png)

    • 在配置页面我们可以看到authtoken,根据authToken启动natapp客户端,进入natapp所在的目录,打开命令行输入如下命令:

      propertie
      natapp -authtoken=你的authtoken

      ![](支付 - 改.assets/启动natapp.png)

2.6.2 测试 ​

创建springboot工程,版本为2.3.0.RELEASE

java
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--导入支付宝支付sdk-->
        <dependency>
            <groupId>com.alipay.sdk</groupId>
            <artifactId>alipay-sdk-java</artifactId>
            <version>4.35.71.ALL</version>
        </dependency>

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.11.1</version>
        </dependency>
    </dependencies>

定义启动类

java
@SpringBootApplication
public class PayApplication {

    public static void main(String[] args) {
        SpringApplication.run(PayApplication.class, args);
    }
}

定义配置类

java
@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient() {
        // 1. new 出一个config对象
        Config config = new Config();
        // 2. 配置Config对象
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress("redis://192.168.153.130:6379");

        // 设置序列化
        config.setCodec(new JsonJacksonCodec());

        // 3. 创建redis客户端 该对象在创建的时候,就会像redis-server发起连接请求
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
    
    @Bean
    public AlipayConfig getPublicConfig() {
        String privateKey = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCwH5CIl0zwjtnxXSVAom43F1QQFOz30V3y/ZswtHKQzC12e1dYkxLqeq7HOVYhBK225MU1WHZWM0he8V4ISGY7nCz0zE179OS8EcNR152MMkYlp6ZjspC6TtkdY/3WDbo/lgC6GN9T1bqiQnpAYL+61DAF4v70iOXVo6vWy/2IBlmG0Nfyn67RswFgmurNa/m/3MhbH3Ncd6hUUB8GnGgjtvXXTHJPniYNZzDUCBdA1YF5wQhG5D9zEpPitM10kPVBd0eJD+uThKQ1ZyKE7Wnb1l8VXIyFEOFkGqylAtCtjhz37F/YMOSn+QLGEZsc77TYwNABneHt1nuFKle8AMhfAgMBAAECggEAEoIOpzv3GuR4JLQcIRGwsVtjOxln2ZcH32wlLdYYn/zE3kmR4T37Y+amjUsKMQgT1T9vNe7o6KAU/90ve4FYNPVxh/wcPGV80AKx2tzksoHp+zUF+D4glWOJz1vdpevlYZ86zlOkzGOObFS+EhvYqiJ4NXYoQrxMIspDWZwwNWYAsqbUl2DITYo+JkBZP6U1oeQCs09KFsoIs73tsLc7UE5JLZL/fjmigo8wJvfGrAXAQwlg0ie8oAzpjlS2hK5YhaRezw9OqgcRb/KgnPOWApAepJ86RYOQsvhiuuP9SLctYSnCOWWxP5f+zLU1Hf1l4RAlctdy255QWaC6lbDHAQKBgQDi3Xl3Ccnv8zQVkIpiNSChjiYtjDELcdqbrdCSbLdUxRvjxg6t1NPVRTI9LzjCSIRWny01pYv6Ujt2+nAW28bGiPcMPg2J+Q5G70H33g85uJ6yy4aPTyfDi0XuUwn0Xaetm/qWFR4Mrvgy2a1eKMWXMN8OpD+Ebk/CKbLsr0kv3wKBgQDGveCk0GY+NOYx791nea9mWlFdY54Zx3HWf/cTnwcDGwr51jTYBgeoiWX0WCC2s4i6awCEedkTz0QdRRze/NLC9gYRwagHFA9rUwENYFih4HPiFcz13DjN6I0pjLam1VQf37ZWJyRwXXf4J2trMBDYFz8l84IPnxQWn2m7LQF3gQKBgQCWYVzMrW5wYfQaf09bvf+9V26zLoSsI3JXU6Y4CVyVEntkRrsgOz2X12Bv8kdbcZpXmPfs4amh6rSEL4nxfQmMPOoV8WQkGzV9i8dcuJO7HUgFGKg/gqbHFiDq05x7oUEu8X/v0Fu06J6ZhnVHPxuLFtgk6nc4H686800pWx/WXQKBgQCY3Bx3x86MFBXl3M8XMnHlMJyaTu+gdlWpnN0GG2/CRL+Jb+dPLDwhtiRT7qCixa3pbDmGq016vhVuyeSt4hmdWKtMZv39C8HcU4hgqHUjdMbM4uW1SL/sJ+zDQ3aNFVHR/jh5RTvyrQGEPZWSaPLbse2hHA0yRLGnwM8K50/UgQKBgHgd0L619bDKRsBKRag4fquNn+oU1kgWX8jjpGcLcDP/Eqpc7eE7AFSkMMuRS9aL8f59gyC4uv1DyuYpx3pgiPKER82uEkeLRrLGxJ31EtmOtc9XshOEw6v1x0MqkELVwCuJmf/DiW8MYEZDN07rkrx7doxKFBV+FrZQ5oJPnoCW";
        String alipayPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqnyvBrR4QWg0vY30uYToeDyZ7mq71nYVgkUKsBqaCzKoGbrGt75kWw7r+07ZHI8CqiYeQGd9krQ7TkFZ/DYF4p0k86wn21kqAILAVBW5mDkZF8xznDXX754NIcRXyDeUBuwGQ+FtH3GWtQVHAoTkv9XTW3RWonC/9z7DumDG3CajKZGUq64PnX0xttfEAIkGnGd7jgT8TQCVC6CH31XQMvOSDmkpCMNKYQn/XNpwaoV+SlXpJ8vV7a/9iThGBhXXRceilzOhK1bAB6MBFaMu67KWOV075cP3AwCHJmJyPVxzK54h/zn9wx0gEFGMAsuWaMTS6QI6VJCODKLwOdkpKQIDAQAB";
        AlipayConfig alipayConfig = new AlipayConfig();
        alipayConfig.setServerUrl("https://openapi.alipaydev.com/gateway.do");
        alipayConfig.setAppId("2021000117621153");
        alipayConfig.setPrivateKey(privateKey);
        alipayConfig.setFormat("json");
        alipayConfig.setAlipayPublicKey(alipayPublicKey);
        alipayConfig.setCharset("UTF8");
        alipayConfig.setSignType("RSA2");
        return alipayConfig;
    }
}

定义Controller

java
@RestController
public class payController {
    
    @Autowired
    AlipayConfig alipayConfig;


     @GetMapping("/test/page/pay")
    public String testPagePay() throws AlipayApiException {

        // 创建AlipayAlipayClient
        AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig);
        // 创建Request对象
        AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
        // 创建Model对象封装请求参数
        AlipayTradePagePayModel model = new AlipayTradePagePayModel();
        model.setOutTradeNo("cskaoyan005");
        model.setTotalAmount("88.88");
        model.setSubject("Iphone6 16G");
        model.setProductCode("FAST_INSTANT_TRADE_PAY");

        request.setNotifyUrl("https://732sw00878.zicp.fun/callback/notify");
        // 给Model对象
        request.setBizModel(model);
        // 发起请求,获取结果
        AlipayTradePagePayResponse response = alipayClient.pageExecute(request);

        if (response.isSuccess()) {
            System.out.println("调用成功");
        } else {
            System.out.println("调用失败");
        }
        // 输出响应体(支付表单页面)
        return response.getBody();
    }
    
    
    @GetMapping("/test/pay/query")
    public String testPayQuery() throws AlipayApiException {
        // 创建AlipayAlipayClient
        AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig);
        // 创建Request对象
        AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
        // 创建Model对象封装具体的请求参数
        AlipayTradeQueryModel model = new AlipayTradeQueryModel();
        model.setOutTradeNo("cskaoyan005");
        // 给Request设置具体的请求
        request.setBizModel(model);
        // 发起请求并获取响应
        AlipayTradeQueryResponse response = alipayClient.execute(request);

        if (response.isSuccess()) {
            System.out.println("调用成功");
        } else {
            System.out.println("调用失败");
        }


        // 返回响应中查询到的订单支付状态
        return response.getTradeStatus();
    }
    
    @GetMapping("return/notify")
    public String returnCallback(@RequestParam Map<String, String> paramsMap) {
        System.out.println(paramsMap);
        return "return notify success";
    }

    @GetMapping("/test/pay/close")
    public String testClosed() throws AlipayApiException {
        // 创建AlipayAlipayClient
        AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig);
        // 创建Request对象
        AlipayTradeCloseRequest request = new AlipayTradeCloseRequest();
        // 创建Model对象封装具体的请求参数
        AlipayTradeCloseModel model = new AlipayTradeCloseModel();
        model.setOutTradeNo("cskaoyan005");
        // 给Request设置具体的请求
        request.setBizModel(model);
        // 发起请求并获取响应
        AlipayTradeCloseResponse response = alipayClient.execute(request);

        if (response.isSuccess()) {
            System.out.println("调用成功");
        } else {
            System.out.println("调用失败");
        }
        // 输出响应体
        return response.getBody();
    }

    @Autowired
    RedissonClient redissonClient;

    @PostMapping("/callback/notify")
    public String notifyCallback(@RequestParam Map<String, String> paramsMap) throws AlipayApiException {
  @Autowired
    RedissonClient redissonClient;

    @PostMapping("/callback/notify")
    public String notifyCallback(@RequestParam Map<String, String> paramsMap) throws AlipayApiException {

        // 1. 调用SDK验证签名
        boolean signVerified = AlipaySignature.rsaCheckV1(paramsMap, alipayConfig.getAlipayPublicKey(), alipayConfig.getCharset(), alipayConfig.getSignType());

        // 2. 验签成功后,按照支付结果异步通知中的描述,对支付结果中的业务内容进行二次校验
        if (signVerified) {

            String outTradeNo = paramsMap.get("out_trade_no");  // 外部订单号
            String totalAmount = paramsMap.get("total_amount"); // 交易总金额
            String appId = paramsMap.get("app_id");   // appId
            String tradeStatus = paramsMap.get("trade_status");  // 交易状态

            // 3. 二次参数校验,校验金额,校验appid
            // 根据订单id去数据库获取订单信息,以及其中的订单金额
            BigDecimal dbTotalAmount = new BigDecimal("88.88");
            int result = new BigDecimal(totalAmount).compareTo(dbTotalAmount);
            if (!appId.equals(alipayConfig.getAppId()) || result != 0) {
                return "failure";
            }
            // 4. 交易状态校验
            if (tradeStatus != null && (tradeStatus.equals("TRADE_SUCCESS") || tradeStatus.equals("TRADE_FINISHED"))) {

                // 5. 幂等性校验
                String notifyId = paramsMap.get("notify_id");
                String key = "pay:callback:notifyid" + notifyId;
                RBucket<String> bucket = redissonClient.getBucket(key);
                boolean ret = bucket.trySet(notifyId, 25, TimeUnit.HOURS);
                if (!ret) {
                    System.out.println("failure");
                    return "failure";
                }

                // 执行业务逻辑
                System.out.println("success");
                return "success";
            } else {
                System.out.println("failure");
                return "failure";
            }
        } else {
            System.out.println("failure");
            return "failure";
        }
    }

}

3. 表结构设计 &#8203;

sql
CREATE TABLE `payment_info` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
  `out_trade_no` varchar(50) DEFAULT NULL COMMENT '对外业务编号,创建订单的时候生成,从订单中获取',
  `order_id` varchar(50) DEFAULT NULL COMMENT '订单编号',
  `user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
  `payment_type` varchar(20) DEFAULT NULL COMMENT '支付类型(微信 支付宝)',
  `total_amount` decimal(10,2) DEFAULT NULL COMMENT '支付金额',
  `subject` varchar(200) DEFAULT NULL COMMENT '交易内容,利用商品名称拼接',
  `payment_status` varchar(20) DEFAULT NULL COMMENT '支付状态',
    
  `trade_no` varchar(50) DEFAULT NULL COMMENT '交易编号,回调时设置',
  `callback_time` datetime DEFAULT NULL COMMENT '回调时间',
  `callback_content` text COMMENT '回调信息',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `is_deleted` tinyint(3) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1001 DEFAULT CHARSET=utf8 COMMENT='支付信息表';

4. 接口 &#8203;

4.1 去支付 &#8203;

![](支付 - 改.assets/获取支付二维码页面.png)

java
@RequestMapping("/pay/alipay/submit/{orderId}")
@ResponseBody
public String submitOrder(@PathVariable Long orderId){
    
    String form = alipayService.createAliPay(orderId);
    // 在上面这个支付的方法中,需要做的事情有如下几个
    
    // 1. 校验支付对应的订单状态是否为未支付,如果是已支付或已关闭,则直接返回
    
    // 2. 保存支付记录到支付表
    
    // 3. 调用支付宝SDK,生成支付表单(支付表单实际上就是一个支付页面,是一个html的字符串)
    
    // 4. 返回支付表单
    return form;
}

这里有一点需要注意,在上面的第3步生成表单的时候,考虑到订单超时取消功能的实现,我们需要设置支付表单的过期时间,需要和订单自动超时时间同步 或者是 比订单自动超时取消时间 稍晚一点(比如晚1s)即可。

4.2 支付完成 &#8203;

需要说明的是,同步回调和异步回调都会携带参数,在当前项目中,我们使用同步回调来跳转到支付成功页面;使用异步回调来修改订单状态等。这也是支付宝推荐的使用方式,新版本的支付接口已经取消了同步回调的支付结果的传递。

image-20230326184921170

同步回调 &#8203;

同步回调的作用是跳转到支付成功页面。

![](支付 - 改.assets/同步通知处理链路.png)

pay-service#PayApiController

java
/**
 * 支付成功:同步回调
 */
@RequestMapping("/pay/alipay/alipay/callback/return")
public String callBack() {
    log.info("支付成功,同步回调! ");
    // 同步回调给用户展示信息
    return "redirect:" + "http://localhost:8000/#/pay/success";
}

异步回调 &#8203;

异步回调是指在请求参数中传入 notify_url 参数,在用户支付成功后,支付宝服务器会按照这个异步地址使用 post 方式给 notify_url 来发送交易信息,参数示例。需要注意的是:notify_url 地址由商户自己定义保证可以正常使用外网 post 方式访问,否则是无法正常接收到异步通知的。

![](支付 - 改.assets/异步通知链路.png)

pay-service#PayApiController

java
/**
 * 支付成功:异步回调
 */
@PostMapping("/pay/alipay/callback/notify")
@ResponseBody
@SneakyThrows
public String callbackNotify(@RequestParam Map<String, String> paramsMap){
    log.info("支付成功,异步回调,paramMap:{}", JSON.toJSONString(paramsMap));

    // 验证参数
    	// 验签
    	// 校验金额
    	// 校验appid
    	// 校验交易状态
    	// 幂等性校验
    
     // 1. 调用SDK验证签名
        boolean signVerified = AlipaySignature.rsaCheckV1(paramsMap, alipayConfig.getAlipayPublicKey(), CsmallAlipayConfig.charset, CsmallAlipayConfig.sign_type);

        // 2. 验签成功后,按照支付结果异步通知中的描述,对支付结果中的业务内容进行二次校验
        if (signVerified){

            String outTradeNo = paramsMap.get("out_trade_no");  // 外部订单号
            String totalAmount = paramsMap.get("total_amount"); // 交易总金额
            String appId = paramsMap.get("app_id");   // appId
            String tradeStatus = paramsMap.get("trade_status");  // 交易状态

            PaymentInfoDTO paymentInfo = payService.queryPaymentInfoByOutTradeNoAndPaymentType(outTradeNo, PaymentType.ALIPAY.name());

            // 3. 二次参数校验,校验金额,校验appid
            if (paymentInfo == null) return "failure";
            BigDecimal dbTotalAmount = paymentInfo.getTotalAmount();
            double m = Double.valueOf(totalAmount) - dbTotalAmount.doubleValue();
            if (!appId.equals(alipayConfig.getAppId()) || m != 0) {
                return "failure";
            }
            // 4. 交易状态校验
            if (tradeStatus != null && (tradeStatus.equals("TRADE_SUCCESS") || tradeStatus.equals("TRADE_FINISHED"))) {

                // 5. 幂等性校验
                String notifyId = paramsMap.get("notify_id");
                String key = RedisConst.PAY_CALL_BACK_VERFY_PREFIX + notifyId;
                RBucket<String> bucket = redissonClient.getBucket(key);
                // setnx
                boolean ret = bucket.trySet(notifyId,RedisConst.PAY_CALL_BACK_EXPIRE_TIME, TimeUnit.SECONDS);
                if (!ret) {
                    // 幂等标记存在,说明处理成功,重复的通知,返回success
                    return "success";
                }
                
                // 6. 修改支付表状态为支付成功 //修改失败的话,需要删除幂等标记
            } else {
                // 通知失败,返回failure
                return "failure"
            }
        }
    
    
    // 回调成功,返回success
    return "success";
}