目录
订单支付
一、订单重复提交
订单重复提交指的是同一个订单多次提交的情况,常见原因如下:
重复点击:用户可能会因为网络延迟、手抖、页面加载缓慢等原因,多次点击下单提交按钮
页面刷新:用户在订单提交后,如果刷新页面,浏览器可能会重新发送上一次的表单提交请求
防止订单重复提交的本质就是保证下单方法不要被重复调用,这首先可以从前端处理
比如在用户点击提交按钮后,立即将按钮禁用或者改为提交中状态,防止用户再次点击
但是前端只能提高用户的体验,不能保证安全,因为用户可以通过一些脚本提交请求,因此还需要后端处理
后端的处理方式有很多,经常用到的就是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);
效果展示:

二、订单支付
在当前项目中,用户下单成功提示用户去支付,支付成功就会提示下单成功
要注意的是,在我们做系统设计的时候,下单和支付是两个隔离的功能

接下来就要研究支付,具体的步骤如下:
调研微信支付,搞清楚微信支付的详细流程以及要调用的API
封装支付功能到一个新的微服务中,就是支付微服务
在订单微服务中去调用支付微服务,然后支付微服务在调用微信完成支付功能

用户使用的是微信小程序,所以支付过程是使用微信支付,官方入口:
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
下面是微信扫码支付的时序图,我们可以根据它熟悉微信扫码支付的流程

整个流程可以划分为三个步骤:
后台系统在下单完成后,调用微信官方获取支付链接,放到二维码中返回给用户
用户扫描二维码,输入付款密码,将钱付给微信官方,并接收微信官方返回的支付结果
后台系统接收微信官方返回的支付结果,这里有两种方式
被动通知:后台系统中提供接口,微信官方会调用此接口来通知支付的状态
主动查询:后台系统主动调用微信官方提供的查询接口来获取支付的状态
此过程中使用到的微信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;
}
效果展示:

浙公网安备 33010602011771号