前几天对实习还是继续学习技术产生了抉择,问了一个前辈,他抛给我一个问题,怎么做15分钟订单自动取消,我说然后到时间之后,自动执行这个订单关闭业务,比如把锁了的库存给解开等等操作,然后在数据库里肯定要有体现,比如抖音,查我的订单内一块,在前段应该显示已过期,把支付的按钮变灰或者取消,支付的时候不能直接对库存进行操作,应该是支付模块调用订单模块调用库存模块。
然后把理论予以实现,发现需要考虑的点还是有一些的。
功能架构
该功能使用了三大模块解除耦合,保证高可用性,从高层到底层分别是支付模块->订单模块->商品模块,各司其职,支付模块主要负责支付,退款等相应功能;订单模块主要负责订单查询,订单创建,删除订单相关功能;商品模块主要负责商品上下架,补货,查货相关功能;是环环相扣的,支付模块通过操控订单实现支付,订单模块通过操控商品来实现订单创建等操作。
功能实现流程
由于是全栈开发,本功能实现流程会涵盖前后端都有。由于管理模块,就是添加商品等等操作,在前端并没有实现,就先在数据库里加一些商品。sql如下:
然后写一个接口获取商品列表,然后前端进行渲染,效果如下:
测试阶段,未接入微信支付等,但有字段标识,后期方便扩展,渲染的时候只需要有一点,判断支付类型,如果是钱支付就渲染成¥的形式,如果是积分就渲染成积分的形式。然后点击兑换会弹出一个弹出框如下:
扩展一些信息也会变得非常简单,由于数据库里设置了两个字段一个是销售价(也就是优惠后的价格),一个是原价,直接在前端算出优惠金额即可。然后点击提交订单,就会向后端去发送一个创建的请求,java代码如下所示:
java
// 这里先把订单持续事件设置成15秒
// 1.1 先校验参数,看看该商品是否有库存
CommonGoodPo good = goodMapper.queryGoodById(dto.getGoodId());
if (good == null) return Result.error("已查询不到该商品");
Boolean isPut = good.getIsPut();
if (!isPut) return Result.error("无法对已下架的商品下订单");
// 1.2 看看库存还够不够
Boolean haveStore = good.getHaveStore();
Integer store = good.getStore();
if (haveStore && store <= 0) return Result.error("库存不足,无法下单");
// 库存足就锁定一个库存
if (haveStore) goodMapper.lockStore(good.getId());
// 1.3 todo 如果有一个人只能买一单等等限制,可继续扩展
// 2.1 然后就要生成订单的信息了,比如订单的id,这里起始应该使用杂乱的数字字母等作为id,图方便就直接使用自增id
// 2.2 然后生成开始时间和结束时间
LocalDateTime now = LocalDateTime.now();
LocalDateTime end = LocalDateTime.now().plusMinutes(15); // 15分钟写死
PopOrderVo popOrderVo = new PopOrderVo(null, good.getId(), now, end, 15 * 60, dto.getAccount());
orderMapper.creatOrder(popOrderVo);
// 3 准备对象,发送消息
String mes = JSON.toJSONString(popOrderVo);
sendDelayMessage(mes, 60 * 15, RabbitConstants.DELAY_ORDER_DEL_DIRECT_EXCHANGE, RabbitConstants.DELAY_ORDER_KEY);
// 然后封装一下给前端,然后前端得到信息后,弹出弹出层,在我的订单里也可以找到该信息
return Result.ok(popOrderVo);
代码逻辑主要分为三部分,校验参数;然后去执行sql,就是向数据库的order表里去添加一条订单消息,并且要锁库存;最后向rabbitMQ里发一条延迟消息,这个是封装好的方法。到期执行的方法如下所示:
java
log.error("我要开始喽!");
// 把msg转成对象
PopOrderVo popOrderVo = null;
try {
popOrderVo = JSON.parseObject(msg, PopOrderVo.class);
} catch (Exception e) {
throw new RuntimeException("请调用符合规定的api,类型转换错误,无法转换为PopOrderVo");
}
// 自动取消订单逻辑
LocalDateTime now = LocalDateTime.now();
Integer orderId = popOrderVo.getOrderId();
Integer goodId = popOrderVo.getGoodId();
// 修改订单状态
orderMapper.finishOrder(orderId, now, ORDER_STATUS_OVERTIME);
// 把锁了的库存释放
goodMapper.unLockStore(goodId);
log.error("我结束喽!");
就是去校验一下参数,然后去解锁库存,然后修改订单状态。
订单表如下所示:
然后在点击提交订单后,弹出支付页面,并有前端实现的倒计时。
然后会显示一些订单信息,倒计时到了之后,按钮会变成灰色,并无法点击,点击支付按钮后,进入到后端逻辑。代码如下:
java
@Override
@Transactional
public Result payOrder(PayOrderDto dto) {
Integer orderId = dto.getOrderId();
OrderPo orderInfo = orderMapper.getOrderInfo(orderId);
Integer goodId = orderInfo.getGoodId();
LocalDateTime now = LocalDateTime.now();
CommonGoodPo good = goodMapper.queryGoodById(goodId);
// 1 先需要校验参数
// 1.1 看过没过期,如果现在的时间在截止时间之后,则证明过期了
if (now.isAfter(orderInfo.getEndTime())) {
return Result.error("该订单已经过期,请重新下单!");
}
// 1.2 看一下状态是不是待支付状态
if (orderInfo.getStatus() != 0) return Result.error("该订单已被处理,请刷新重试");
// 1.3 看余额够不够
Boolean isEnough = payMapper.checkBalance(orderInfo.getAccount(), good.getSalePrice());
// 2.1 如果不够,直接报错余额不足
if (!isEnough) return Result.error("你的余额不足,无法完成支付");
// 2.2 如果够,扣减余额
payMapper.decreaseBalance(orderInfo.getAccount(), good.getSalePrice());
// 2 需要进行三个方面的处理
// 2.1 商品给增加销量等等
goodMapper.increaseSales(goodId);
// 2.2 设置订单状态,截至时间等
orderMapper.finishOrder(orderId, now, ORDER_STATUS_SUCCESS);
// 2.3 记录支付日志
payMapper.logPay(good.getSalePrice(), good.getPayType(), orderInfo.getAccount(), now, goodId);
// return null;
return Result.ok("支付成功");
}
支付逻辑比较简单,就是校验参数,但需要注意需要检验一下订单状态,因为可能因为网络问题等等,导致请求到后端的时候,出现时间延迟问题,出现状态已经不是待支付状态已经被修改的问题(处理幂等问题),余额不足问题,这时就不能再进行支付,要对订单进行一下处理。如果满足条件就进行sql操作。
然后在前端为了订单的丢失,又写了一个我的订单页面,如下所示:
可以根据不同的状态去筛选不同的订单列表,然后并可以在此页面完成支付。
至此本功能结束,使用的是rabbitMQ的延迟队列实现定时操作,也可以使用xxl-job,直接使用策略模式扩展一下即可,在复习专栏里有关于设计模式的文章和例子,后续有时间会讲一下定时操作策略的扩展。