目录

订单支付

一、订单重复提交

Redission

二、订单支付

支付服务

1)下单

2)查询支付状态

3)退款

订单支付代码实现:


订单支付

一、订单重复提交

订单重复提交指的是同一个订单多次提交的情况,常见原因如下:

  • 重复点击:用户可能会因为网络延迟、手抖、页面加载缓慢等原因,多次点击下单提交按钮

  • 页面刷新:用户在订单提交后,如果刷新页面,浏览器可能会重新发送上一次的表单提交请求

防止订单重复提交的本质就是保证下单方法不要被重复调用,这首先可以从前端处理

比如在用户点击提交按钮后,立即将按钮禁用或者改为提交中状态,防止用户再次点击

但是前端只能提高用户的体验,不能保证安全,因为用户可以通过一些脚本提交请求,因此还需要后端处理

后端的处理方式有很多,经常用到的就是Token机制分布式锁

1)Token机制

  • 生成Token:用户每次进入下单页面,前端会向服务器请求一个唯一的Token

  • 存储Token:后端将生成Token存放在Redis中,并设置一定的过期时间,然后给前端返回

  • 验证Token:用户提交订单时需要携带Token,服务器验证Token,一旦Token被使用过就失效

2)分布式锁

分布式锁是在分布式系统下的锁机制,旨在保证代码在加锁期间只能被同一个线程访问

实现分布式锁的方案有很多,比如下面这些:

  • MySQL:可以利用MySQL主键、唯一列不能保存重复数据这一特性做分布式锁

  • Redis:可以利用setnx命令无法向Redis重复保存同一key的特性做分布式锁

Redission

Redisson是一个高级的Redis客户端,借助它可以轻松的实现使用Redis做分布式锁、延迟消息队列等功能

相比于RedisTemplate之类的客户端,它更侧重于对Redis功能的模块化封装

RedissonClient是Redisson的客户端对象,核心方法如下:

  • 定义锁:getLock(锁)

  • 获取锁:tryLock(获取锁的超时时间, 持有锁的最大时间, 时间单位)

  • 释放锁:unlock()

注意:释放锁的代码一定要写在finally中,保证锁一定可以得到释放

将资料中的demo-redisson.zip解压后导入到Idea中,打开测试类,学习它的核心API

1)创建客户端连接对象

@Configuration
public class RedissonConfiguration {
    @Bean
    public RedissonClient redissonClient() {
        // 创建配置类,设置连接地址和密码
        Config config = new Config();
        config.useSingleServer()
            .setAddress("redis://192.168.101.68:6379")
            .setPassword("redis");
        // 创建客户端
        return Redisson.create(config);
    }
}

2)在测试类中测试分布式锁的使用

@SpringBootTest
public class RedissonTest {
    @Autowired
    private RedissonClient redissonClient;
    @Test
    public void test() {
        //模拟启动三个线程去获取锁
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                //定义锁对象,参数就是锁的标识
                RLock lock = redissonClient.getLock("LOCK");
                try {
                    //尝试获取锁对象
                    //参数1: 获取锁的超时时间
                    //参数2: 持有锁的最大时间
                    //参数3: 时间单位
                    //返回结果代表获取锁是否成功
                    boolean flag = lock.tryLock(3, 10, TimeUnit.SECONDS);
                    if (!flag) {
                        throw new RuntimeException("请求太频繁");
                    }
                    //模拟业务逻辑代码
                    System.out.println(Thread.currentThread().getId() + "执行逻辑");
                    //休眠
                    ThreadUtil.sleep(1, TimeUnit.MINUTES);
                } catch (Exception e) {
                    System.out.println(Thread.currentThread().getId() + "抢锁失败");
                } finally {
                    lock.unlock();//释放锁
                }
            }).start();
        }
        //不让主线程退出
        ThreadUtil.sleep(30000000);
    }
}

在OrderController中修改下单方法,实现需求:30秒内只能有一个线程下单成功

@Autowired
private RedissonClient redissonClient;
@GetMapping("/placeOrder")
public void placeOrder(){
    //定义锁对象
    RLock lock = redissonClient.getLock("LOCK");
    try {
        //尝试获取锁对象
        //参数1: 获取锁的超时时间
        //参数2: 持有锁的最大时间
        //参数3: 时间单位
        boolean flag = lock.tryLock(1, 30, TimeUnit.SECONDS);
        if (!flag) {
            throw new RuntimeException("请求太频繁");
        }
        //执行下单业务逻辑,耗时3秒
        System.out.println(Thread.currentThread().getId() + "下单成功");
        ThreadUtil.sleep(3000);
    } catch (Exception e) {
        System.out.println(Thread.currentThread().getId() + "下单失败");
    } finally {
        //不去手动释放锁,代表是让锁等到最大持有时间(30秒)后,自动释放
        //lock.unlock();//释放锁
    }
}

上一步的代码虽然可以实现防止重复下单的功能,但是代码繁琐,我们考虑使用AOP的方式来进行优化

当前提供的工程中已经提供好了一个切面类LockAspect,在类中实现的加锁和解锁的逻辑

我们可以直接在我们的方法中使用它来简化下单的方法,具体实现如下

@RestController
public class OrderController {
    // formatter: 锁标识
    // waitTime: 获取锁的超时时间,默认单位是秒
    // time: 持有锁的最大时间,默认单位是秒
    // unlock: 方法执行完毕是否需要自动释放锁
    @Lock(formatter = "Lock2", waitTime = 1, time = 30, unlock = false)
    @GetMapping("/placeOrder")
    public void placeOrder() {
        //执行下单业务逻辑,耗时3秒
        System.out.println(Thread.currentThread().getId() + "下单成功");
        ThreadUtil.sleep(3000);
    }
}

AOP当中切面的核心代码:

当前项目中的需求是,同一个用户对同一个服务,不能在短时间内重复下单,因此我们设计如下:

用户id+服务id作为分布式锁标识锁定下单接口30秒,则30秒内该用户不能再次对该服务下单

修改下单方法,执行下单方法前获取分布式锁,锁的过期时间设置为30秒,使用项目封装的Lock注解实现

锁名称:ORDERS:CREATE:LOCK:用户id:服务id

@Override
public PlaceOrderResDTO placeOrder(PlaceOrderReqDTO placeOrderReqDTO) {
    return owner.placeOrder(UserContext.currentUserId(), placeOrderReqDTO);
}
@Lock(formatter = "ORDERS:CREATE:LOCK:#{userId}:#{placeOrderReqDTO.serveId}", time = 30, waitTime = 1,unlock=false)
public PlaceOrderResDTO placeOrder(Long userId, PlaceOrderReqDTO placeOrderReqDTO) {
     //...源代码逻辑不变
}

IOrdersCreateService中也需要添加对应的接口声明

/**
 * 创建订单
 *
 * @param userId 登录用户id
 * @param placeOrderReqDTO 订单参数
 * @return 订单id
 */
PlaceOrderResDTO placeOrder(Long userId, PlaceOrderReqDTO placeOrderReqDTO);

效果展示:

二、订单支付

在当前项目中,用户下单成功提示用户去支付,支付成功就会提示下单成功

要注意的是,在我们做系统设计的时候,下单和支付是两个隔离的功能

接下来就要研究支付,具体的步骤如下:

  1. 调研微信支付,搞清楚微信支付的详细流程以及要调用的API

  2. 封装支付功能到一个新的微服务中,就是支付微服务

  3. 在订单微服务中去调用支付微服务,然后支付微服务在调用微信完成支付功能

用户使用的是微信小程序,所以支付过程是使用微信支付,官方入口:

https://pay.weixin.qq.com/static/product/product_index.shtml

我们项目中最符合使用的是小程序支付,它对应的文档地址如下:

https://pay.weixin.qq.com/doc/v3/merchant/4012791894

但是它需要企业用户才能用,所以我们使用扫码支付给大家演示流程,它对应的文档地址如下:

https://pay.weixin.qq.com/doc/v3/merchant/4012791874

下面是微信扫码支付的时序图,我们可以根据它熟悉微信扫码支付的流程

整个流程可以划分为三个步骤:

  1. 后台系统在下单完成后,调用微信官方获取支付链接,放到二维码中返回给用户

  2. 用户扫描二维码,输入付款密码,将钱付给微信官方,并接收微信官方返回的支付结果

  3. 后台系统接收微信官方返回的支付结果,这里有两种方式

      被动通知:后台系统中提供接口,微信官方会调用此接口来通知支付的状态

      主动查询:后台系统主动调用微信官方提供的查询接口来获取支付的状态

    此过程中使用到的微信API有:

    • Native下单:https://pay.weixin.qq.com/doc/v3/merchant/4012791877

    • 微信支付订单号查询订单:https://pay.weixin.qq.com/doc/v3/merchant/4012791879

    支付服务

    一个企业通常有很多系统,这些系统有很多都有支付的需求,因此我们会抽取一个支付微服务

    然后将所有支付相关的内容放进去,这样哪个项目用到了,就直接调用即可

    将资料中提供的jzo2o-trade.zip解压到自己的代码目录,用IDEA打开

    支付微服务中提供了关于交易的四个核心类:

    • NativePayController:提供支付相关的功能

    • RefundRecordController:提供退款相关的功能

    • TradingController:提供交易流水查询相关的功能

    • NotifyController:提供支付结果回调相关的功能

    此微服务对应的数据库在虚拟机中已经提供,核心表有下面三张

    • 支付渠道表(pay_channel):存储微信、支付宝的支付参数,如:商户号、证书序列号、api私钥等

    • 交易单表(trading):存储用户支付的记录

    • 退款表(refund_record):存储用户申请退款的记录

    还有一个是AT模式的快照表

    1)下单

    接口描述:调用此接口可以完成下单功能(它底层对接了微信的统一下单接口),并返回支付链接二维码

    代码位置:NativePayController.createDownLineTrading()

    请求路径:POST /trade/inner/native/

    请求参数:

    返回结果:

    这里qrCode的内容用html的img标签可以打开为微信支付二维码:

    2)查询支付状态

    接口描述:调用此接口可以根据交易订单号查询订单状态(它底层对接了微信的订单查询接口)

    代码位置:TradingController.findTradResultByTradingOrderNo()

    请求路径:GET /trade/inner/tradings/findTradResultByTradingOrderNo

    请求参数:Long tradingOrderNo 交易系统订单号(由下单接口返回)

    返回结果:

    3)退款

    接口描述:调用此接口可以根据交易订单号完成退款(它底层对接了微信的退款接口)

    代码位置:RefundRecordController.refundTrading()

    请求路径:POST trade/inner/refund-record/refund

    请求参数:Long tradingOrderNo 交易系统订单号(由下单接口返回),BigDecimal refundAmount 退款金额

    返回结果:

    订单支付代码实现:

    ConsumerOrdersController:

    @ApiOperation("订单支付")
        @PutMapping("/pay/{id}")
        public OrdersPayResDTO pay(@PathVariable Long id, @RequestBody OrdersPayReqDTO ordersPayReqDTO){
            return ordersCreateService.pay(id, ordersPayReqDTO);
        }

    IOrdersCreateService:

    /**
     * 订单支付
     *
     * @param id              订单id
     * @param ordersPayReqDTO 支付请求对象
     * @return 支付响应
     */
    OrdersPayResDTO pay(Long id, OrdersPayReqDTO ordersPayReqDTO);

    OrdersCreateServiceImpl:

    @Autowired
    private NativePayApi nativePayApi;
    @Autowired
    private TradeProperties tradeProperties;
    @Override
    public OrdersPayResDTO pay(Long id, OrdersPayReqDTO ordersPayReqDTO) {
        //调用支付微服务 获取 二维码图片
        //1. 根据订单id查询订单信息,如果订单不存在, 直接返回错误
        Orders orders = this.getById(id);
        if (ObjectUtil.isNull(orders)) {
            throw new ForbiddenOperationException("订单不存在");
        }
        //2. 查询订单支付状态, 如果是已经支付 , 直接返回错误
        //transaction_id : 只有支付成功,才会有这个号
        if (orders.getPayStatus() == 4 && StringUtils.isNotEmpty(orders.getTransactionId())) {
            throw new ForbiddenOperationException("订单已经支付了");
        }
        //3. 调用支付微服务, 获取二维码
        NativePayReqDTO nativePayReqDTO = new NativePayReqDTO();
        nativePayReqDTO.setProductAppId("jzo2o.orders");//业务系统标识
        nativePayReqDTO.setProductOrderNo(id);//业务系统订单号
        nativePayReqDTO.setTradingChannel(ordersPayReqDTO.getTradingChannel());//支付渠道
        nativePayReqDTO.setTradingAmount(orders.getRealPayAmount());//支付金额
        nativePayReqDTO.setMemo(orders.getServeItemName());//备注
        //根据交易渠道设置商户号
        if (ObjectUtil.equal(ordersPayReqDTO.getTradingChannel(), PayChannelEnum.WECHAT_PAY)) {
            nativePayReqDTO.setEnterpriseId(tradeProperties.getWechatEnterpriseId());//微信商户号
        }
        if (ObjectUtil.equal(ordersPayReqDTO.getTradingChannel(), PayChannelEnum.ALI_PAY)) {
            nativePayReqDTO.setEnterpriseId(tradeProperties.getAliEnterpriseId());//阿里商户号
        }
        //原有的交易渠道不为空 而且跟刚刚传入交易渠道不一样
        if (StringUtils.isNotEmpty(orders.getTradingChannel()) &&
                !StringUtils.equals(orders.getTradingChannel(), ordersPayReqDTO.getTradingChannel().toString())
        ) {
            nativePayReqDTO.setChangeChannel(true);//是否改变交易渠道
        }else {
            nativePayReqDTO.setChangeChannel(false);//是否改变交易渠道
        }
        NativePayResDTO payResDTO = nativePayApi.createDownLineTrading(nativePayReqDTO);
        //4. 更新订单表数据(支付服务交易单号 支付渠道)
        orders.setTradingOrderNo(payResDTO.getTradingOrderNo());//支付服务交易单号
        orders.setTradingChannel(payResDTO.getTradingChannel());//支付渠道
        this.updateById(orders);
        //5. 封装返回结果
        OrdersPayResDTO ordersPayResDTO = BeanUtil.copyProperties(payResDTO, OrdersPayResDTO.class);
        ordersPayResDTO.setPayStatus(2);//支付状态: 未支付
        return ordersPayResDTO;
    }

    效果展示:


    ConsumerOrdersController:

    @ApiOperation("查询订单支付结果")
    @GetMapping("/pay/{id}/result")
    public OrdersPayResDTO payResult(@PathVariable("id") Long id) {
        return ordersCreateService.getPayResultFromTradServer(id);
    }

    IOrdersCreateService:

    /**
     * 请求支付服务查询支付结果
     *
     * @param id 订单id
     * @return 支付结果
     */
    OrdersPayResDTO getPayResultFromTradServer(Long id);

    OrdersCreateServiceImpl:

    @Autowired
    private TradingApi tradingApi;
    @Override
    public OrdersPayResDTO getPayResultFromTradServer(Long id) {
        //1. 根据订单id查询订单信息,如果订单不存在, 直接返回错误
        Orders orders = this.getById(id);
        if (ObjectUtil.isNull(orders)) {
            throw new ForbiddenOperationException("订单不存在");
        }
        //2. 如果订单的支付状态是待支付 并且 支付服务交易单号 不为空  调用支付服务查询订单支付状态
        if (orders.getPayStatus() == 2 && orders.getTradingOrderNo() != null){
            //调用支付服务查询订单支付状态
            TradingResDTO tradingResDTO = tradingApi.findTradResultByTradingOrderNo(orders.getTradingOrderNo());
            //根据支付服务返回的状态修改订单表中字段(订单状态、支付状态、第三方支付交易号)
            //交易状态: 2-付款中 3-付款失败 4-已结算 5-取消订单
            TradingStateEnum tradingState = tradingResDTO.getTradingState();
            boolean update = this.lambdaUpdate()
                    //交易状态: 4-已结算  订单状态:派单中
                    .set(ObjectUtil.equal(tradingState, TradingStateEnum.YJS), Orders::getOrdersStatus, OrderStatusEnum.DISPATCHING.getStatus())
                    //交易状态: 3-付款失败  订单状态:已关闭
                    .set(ObjectUtil.equal(tradingState, TradingStateEnum.FKSB), Orders::getOrdersStatus, OrderStatusEnum.CLOSED.getStatus())
                    //交易状态: 5-取消订单  订单状态:已取消
                    .set(ObjectUtil.equal(tradingState, TradingStateEnum.QXDD), Orders::getOrdersStatus, OrderStatusEnum.CANCELED.getStatus())
                    //交易状态: 4-已结算  支付状态:支付成功
                    .set(ObjectUtil.equal(tradingState, TradingStateEnum.YJS), Orders::getPayStatus, OrderPayStatusEnum.PAY_SUCCESS.getStatus())
                    //第三方支付交易单号
                    .set(ObjectUtil.isNotEmpty(tradingResDTO.getTransactionId()), Orders::getTransactionId, tradingResDTO.getTransactionId())
                    //根据订单id更新
                    .eq(Orders::getId, id)
                    .update();
            if (!update) {
                log.info("更新订单:{}状态失败", orders.getId());
                throw new CommonException("更新订单" + orders.getId() + "状态失败");
            }
        }
        //3. 返回结果
        //查询订单的信息
        Orders newOrders = this.getById(id);
        OrdersPayResDTO ordersPayResDTO = new OrdersPayResDTO();
        ordersPayResDTO.setProductOrderNo(newOrders.getId());//业务系统订单号
        ordersPayResDTO.setTradingOrderNo(newOrders.getTradingOrderNo());//交易系统订单号
        ordersPayResDTO.setTradingChannel(newOrders.getTradingChannel());//支付渠道
        ordersPayResDTO.setPayStatus(newOrders.getPayStatus());//支付状态
        return ordersPayResDTO;
    }

    效果展示: