最近再写支付模块就到处借鉴 旨在回顾一下。
1.确认订单功能
使用场景是:用户在选择好购物车后,或者是直接选择商品后(选择商品封装为购物车)
这样做是根据尚硅谷来学习的 目前需要这些属性,原因是在确认订单页面后 展现一个最优惠的状态
1.1实体类
java
/**
* 用户id
*/
@ApiModelProperty("用户id")
private Integer userId;
/**
* 购物车列表
*/
@ApiModelProperty("购物车中已选中商品")
private List<CartInfo> cartItemsList;
@ApiModelProperty("最优惠的优惠券id")
private Integer userCouponsId;
/**
* 总金额
*/
@ApiModelProperty("总金额")
private Double totalAmount;
/**
* 优惠金额
*/
@ApiModelProperty("优惠金额")
private Double discount;
/**
* 实付金额
*/
@ApiModelProperty("实付金额")
private Double actuallyPay;
1.2确认订单实现类
期间做了一些修改 之前是设置的有购物车状态 这些步骤都是在后端处理
后来又采用了前端传递购物车属性 美其名曰叫 减少io次数 缓解数据库压力
通过传入的userId 查找 订单列表 购物车列表
期间有个redis操作 是根据时间 来生成 后续在生成订单时会用到
java
public OrderConfirmVo confirmOrder(CartConfirmDto cartConfirmDto) {
//获取用户id参数 以方便后续使用
//获取用户地址列表
Integer userId = cartConfirmDto.getUserId();
List<Addresses> addressesList = addressesDao.getByUserId(userId);
//获取购物车中已经选中的商品
List<CartInfo> cartItemsList = cartConfirmDto.getCartItemsList();
// List<CartItems> cartItemsList = cartItemsDao.querySelectedCartItems(userId);
if (cartItemsList.isEmpty()){
throw new PorkException("您购物车中未选中商品",500);
}
for (CartInfo cartInfo : cartItemsList) {
Integer productId = cartInfo.getProductId();
Products products = productsDao.queryById(productId);
cartInfo.setMainPhoto(products.getMainPhoto());
cartInfo.setName(products.getName());
Integer flavorId = cartInfo.getFlavorId();
String flavorDescription = flavorsService.queryNameById(flavorId);
cartInfo.setFlavorDescription(flavorDescription);
}
//生成订单唯一标示
String orderNo = System.currentTimeMillis()+"";
redisTemplate.opsForValue().set(RedisConst.ORDER_REPEAT+orderNo,orderNo,24, TimeUnit.HOURS);
//1.获取最优惠优惠券
//2.先获取订单价格
//3.找到实付金额
// Double totalPrice = cartItemsDao.getActuallyPay(userId);
// UserCoupons userCoupon = userCouponsDao.queryOptimalUserCoupon(totalPrice, userId);
// Double discount = userCoupon.getCoupons().getDiscount();
// Double actuallyPay = totalPrice - discount;
Double totalPrice = cartConfirmDto.getTotalAmount();
Integer userCouponId = cartConfirmDto.getUserCouponsId();
Double discount = cartConfirmDto.getDiscount();
Double actuallyPay = cartConfirmDto.getActuallyPay();
UserCoupons userCoupon = userCouponsDao.queryById(userCouponId);
//查询可用优惠券
List<UserCoupons> userCoupons = userCouponsDao.queryAvailableUserCoupons(totalPrice, userId);
//进行封装
OrderConfirmVo orderConfirmVo = new OrderConfirmVo(userId,totalPrice,userCoupon,discount,actuallyPay,orderNo,userCoupons,addressesList,cartItemsList);
return orderConfirmVo;
}
1.3返回类
java
*/
@ApiModelProperty("用户id")
private Integer userId;
/**
* 总金额
*/
@ApiModelProperty("总金额")
private Double totalAmount;
@ApiModelProperty("最优惠的优惠券")
private UserCoupons userCoupons;
/**
* 优惠金额
*/
@ApiModelProperty("优惠金额")
private Double discount;
/**
* 实付金额
*/
@ApiModelProperty("实付金额")
private Double actuallyPay;
/**
* 订单号
*/
@ApiModelProperty("订单号")
private String orderNo;
/**
* 用户所有优惠券
*/
@ApiModelProperty("用户优惠券")
private List<UserCoupons> userCouponsList;
/**
* 用户地址
*/
@ApiModelProperty("用户地址列表")
private List<Addresses> addressesList;
/**
* 购物车列表
*/
@ApiModelProperty("购物车列表")
private List<CartInfo> cartItemsList;
2.生成订单
2.1请求实体类
生成订单后里面的属性
java
@ApiModelProperty(value = "使用预生产订单号防重")
private String orderNo;
@ApiModelProperty(value = "用户id")
private Integer userId;
@ApiModelProperty(value = "下单时所使用的地址信息")
private Integer addressesId;
@ApiModelProperty(value = "下单选中的优惠券id")
private Integer userCouponId;
@ApiModelProperty(value = "订单备注")
private String comment;
@ApiModelProperty(value = "所选中商品")
private List<CartInfo> cartItemsList;
@ApiModelProperty(value = "最后订单总价")
private Double totalPrice;
@ApiModelProperty(value = "优惠金额")
private Double discount;
@ApiModelProperty("订单实付金额")
private Double ActuallyPay;
2.2生成订单方法实体类
使用lua脚本来保证原子性
如果redis中有相同orderNo 则说明正常提交订单 然后把redis删除
期间也有锁单
2.2.1检查锁
java
@Override
public Boolean checkAndLock(List<ProductStockVo> productStockVoList, String orderNo) {
//1.判断productStockVoList是否为空
if (CollectionUtils.isEmpty(productStockVoList)){
throw new PorkException(ResultCodeEnum.DATA_ERROR);
}
//2.遍历productStockVoList得到每个商品,验证库存并锁定库存,具备原子性
productStockVoList.stream().forEach(productStockVo -> {
this.checkLock(productStockVo);
});
//3.只要有一个商品锁定失败,所有锁定成功的商品都解锁 用于检查流中是否至少有一个元素满足指定的条件
boolean flag = productStockVoList.stream()
.anyMatch(productStockVo -> !productStockVo.getIsLock());
if (flag){
//所有锁定成功的商品都解锁
productStockVoList.stream().filter(ProductStockVo::getIsLock)
.forEach(productStockVo -> {
flavorsDao.unlockStock(productStockVo.getFlavorId(),productStockVo.getSkuNum());
});
return false;
}
//4 如果所有商品都锁定成功了,redis缓存相关数据,为了方便后面解锁和减库存
redisTemplate.opsForValue()
.set(RedisConst.SROCK_INFO+orderNo,productStockVoList,33, TimeUnit.MINUTES);
return true;
}
2.2.2获得公平锁
java
private void checkLock(ProductStockVo productStockVo) {
//获取锁 公平锁:谁等待时间长给谁发锁
RLock rLock = this.redissonClient.getFairLock(RedisConst.SKUKEY_PREFIX+productStockVo.getFlavorId());
rLock.lock();
try {
//验证库存
Flavors flavors = flavorsDao.checkStock(productStockVo.getFlavorId(),productStockVo.getSkuNum());
//判断没有满足条件商品,设置isLock值为false,返回
if (flavors == null){
productStockVo.setIsLock(false);
return;
}
//又满足条件商品,锁定库存 update rows 影响行数
Integer rows = flavorsDao.lockStock(productStockVo.getFlavorId(),productStockVo.getSkuNum());
if (rows == 1) {
productStockVo.setIsLock(true);
}
}finally {
//解锁
rLock.unlock();
}
}
2.2.3提交订单
java
public OrderGenerateInfo submitOrder(OrderSubmitVo orderSubmitVo) {
//第一步拿出userId确定给那个用户设置订单
Integer userId = orderSubmitVo.getUserId();
//第二步 订单不能重复提交,重复提交验证
//通过redis + lua 脚本实现 //lua脚本保证原子性
//1.获取传递过来的orderNo
String orderNo = orderSubmitVo.getOrderNo();
if (orderNo.isEmpty()){
throw new PorkException(ResultCodeEnum.ILLEGAL_REQUEST);
}
//2.拿着orderNo到redis中查询 此lua脚本解析 如果redis中存在的值 = 这一个值 那么 这个值 , 不过没有存在 就返回0 然后结束
String script = "if(redis.call('get', KEYS[1]) == ARGV[1]) then return redis.call('del', KEYS[1]) else return 0 end";
//3.如果redis有相同orderNo,表示正常提交订单 ,把redis的orderNo删除
Boolean flag = (Boolean) redisTemplate
.execute(new DefaultRedisScript(script, Boolean.class),
Arrays.asList(RedisConst.ORDER_REPEAT + orderNo),orderNo);
//4.如果redis没有相同orderNo,表示重复提交了,不能再往后进行
if (!flag){
throw new PorkException(ResultCodeEnum.REPEAT_SUBMIT);
}
//第三步 验证库存 并且 锁定库存(订单在30分钟内锁定库存 没有真正减库存)
//获取当前购物车商品
List<CartInfo> cartItemsList = orderSubmitVo.getCartItemsList();
//新建一个锁单Vo 然后把商品信息封装到 Vo里面
if (!CollectionUtils.isEmpty(cartItemsList))
{
List<ProductStockVo> productStockVoList =
cartItemsList.stream().map(item ->{
ProductStockVo productStockVo = new ProductStockVo();
productStockVo.setFlavorId(item.getFlavorId());
productStockVo.setSkuNum(item.getQuantity());
return productStockVo;
}).collect(Collectors.toList());
//验证库存,保证具备原子性 解决超卖问题
Boolean isLockSuccess = flavorsService.checkAndLock(productStockVoList, orderNo);
if (!isLockSuccess){
throw new PorkException(ResultCodeEnum.ORDER_STOCK_FALL);
}
}
//第四步 下单过程
OrderGenerateInfo orderGenerateInfo = this.saveOrder(orderSubmitVo);
//对已生成订单的购物车进行删除
List<Integer> cartIdList = cartItemsList.stream()
.map(CartInfo::getId)
.collect(Collectors.toList());
cartItemsDao.deleteBatchIds(cartIdList);
//1.向两张表中添加数据 order_info order_item
//返回订单id
return orderGenerateInfo;
}
2.2.4保存订单
java
@Transactional(rollbackFor = {Exception.class})
public OrderGenerateInfo saveOrder(OrderSubmitVo orderSubmitVo) {
Integer userId = orderSubmitVo.getUserId();
List<CartInfo> cartItemsList = orderSubmitVo.getCartItemsList();
if (CollectionUtils.isEmpty(cartItemsList)){
throw new PorkException(ResultCodeEnum.DATA_ERROR);
}
List<String> goodInfoList = new ArrayList<String>();
String goodInfo = "";
for (CartInfo cartInfo : cartItemsList) {
Integer flavorId = cartInfo.getFlavorId();
Integer productId = cartInfo.getProductId();
Integer quantity = cartInfo.getQuantity();
String productName = productsService.queryNameById(productId);
String flavorName = flavorsService.queryNameById(flavorId);
goodInfo = productName+":"+flavorName+"*"+quantity;
goodInfoList.add(goodInfo);
}
//查数据 顾客收货地址
Integer addressesId = orderSubmitVo.getAddressesId();
Addresses addresses = addressesDao.queryById(addressesId);
if (addresses == null){
throw new PorkException(ResultCodeEnum.DATA_ERROR);
}
String recipientName = addresses.getRecipientName();
String recipientPhone = addresses.getRecipientPhone();
String province = addresses.getProvince();
String city = addresses.getCity();
String district = addresses.getDistrict();
String detail = addresses.getDetail();
String orderAddress = province + city + district + detail;
//计算金额
Double totalPrice = orderSubmitVo.getTotalPrice();
Double discount = orderSubmitVo.getDiscount();
Double actuallyPay = orderSubmitVo.getActuallyPay();
//原金额
// Double totalAmount = cartItemsDao.getActuallyPay(userId);
// Double discount = 0.00;
// Double actuallyPay = totalAmount;
// Integer couponId = 0;
Integer userCouponId = orderSubmitVo.getUserCouponId();
// UserCoupons userCoupons = userCouponsDao.queryById(userCouponId);
//把优惠券设置为已使用
userCouponsDao.update(userCouponId);
// if (userCoupons!=null){
// couponId = userCoupons.getCouponId();
// }
//优惠券金额
// if (userCouponId != null){
// UserCoupons userCoupons = userCouponsDao.queryById(userCouponId);
// couponId = userCoupons.getCouponId();
// discount = couponsDao.queryById(couponId).getDiscount();
// }
// //实付金额
// actuallyPay = totalPrice - discount;
//封装订单项
List<OrderItems> orderItemsList = new ArrayList<>();
for (CartInfo cartItems : cartItemsList) {
OrderItems orderItem = new OrderItems();
orderItem.setProductId(cartItems.getProductId());
orderItem.setFlavorId(cartItems.getFlavorId());
orderItem.setQuantity(cartItems.getQuantity());
orderItem.setPrice(cartItems.getPrice());
orderItem.setStatus(0);
orderItemsList.add(orderItem);
}
Orders order = new Orders();
order.setUserId(userId);
order.setTotalAmount(totalPrice);
order.setStatus(0);
order.setConsignee(recipientName);
order.setPhone(recipientPhone);
order.setAddress(orderAddress);
order.setDiscount(discount);
order.setOrderNo(orderSubmitVo.getOrderNo());
order.setComment(orderSubmitVo.getComment());
order.setActuallyPay(actuallyPay);
order.setCouponId(userCouponId);
order.setGoodInfo(String.join(", ", goodInfoList));
//添加数据到订单基本表
ordersDao.insert(order);
//添加订单里面的订单项
orderItemsList.forEach(orderItems -> {
orderItems.setOrderId(order.getId());
orderItemsDao.insert(orderItems);
});
//如果当前订单使用优惠券更新优惠券状态
if (order.getCouponId()!= null){
userCouponsDao.update(userCouponId);
}
//在redis中记录用户购物数量
//hash类型 key(userId) - field(skuId)-value(skuNum)
String orderSkuKey = RedisConst.ORDER_SKU_MAP + orderSubmitVo.getUserId();
BoundHashOperations<String, String, Integer> hashOperations = redisTemplate.boundHashOps(orderSkuKey);
cartItemsList.forEach(cartInfo -> {
if(hashOperations.hasKey(cartInfo.getFlavorId().toString())) {
Integer orderSkuNum = hashOperations.get(cartInfo.getFlavorId().toString()) + cartInfo.getQuantity();
hashOperations.put(cartInfo.getFlavorId().toString(), orderSkuNum);
}
});
redisTemplate.expire(orderSkuKey, DateUtil.getCurrentExpireTimes(), TimeUnit.SECONDS);
//设置订单过期时间 30分钟后取消订单
long orderTimeOut = 1;
String keyRedis = String.valueOf(StrUtil.format("{}:{}",RedisConst.REDIS_ORDER_KEY_IS_PAY_0,order.getId()));
//设置过期时间
redisTemplate.opsForValue().set(keyRedis,order.getOrderNo(),orderTimeOut,TimeUnit.MINUTES);
//订单id
OrderGenerateInfo orderGenerateInfo = new OrderGenerateInfo(order.getId(),orderTimeOut);
return orderGenerateInfo;
}
3.讲讲Redis过期键监听器
redis过期键监听器 实现对键的监听
如果该键过期了,则进行注册过的操作
3.1配置监听器
像只注册了订单服务的话 你就只能使用订单服务
若使用其他服务的话 也要进行集成
java
@Configuration
@AllArgsConstructor
public class RedisListenerConfig {
private final RedisTemplate<String, String> redisTemplate;
private final RedissonConfig redisConfigProperties;
private final OrdersService ordersService;
// private final OrderItemsService orderItemsService;
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(new RedisKeyExpirationListener(redisTemplate, redisConfigProperties, ordersService), new PatternTopic(StrUtil.format("__keyevent@{}__:expired", redisConfigProperties.getDatabase())));
return container;
}
}
3.2配置redis 开启监听器
3.3写监听器
java
@Component
public class RedisKeyExpirationListener implements MessageListener {
private RedisTemplate<String, String> redisTemplate;
private RedissonConfig redisConfigProperties;
private OrdersService ordersService;
// private OrderItemsService orderItemsService;
public RedisKeyExpirationListener(RedisTemplate<String, String> redisTemplate,
RedissonConfig redisConfigProperties,
OrdersService orderInfoService
){
this.redisTemplate = redisTemplate;
this.redisConfigProperties = redisConfigProperties;
this.ordersService = orderInfoService;
// this.orderItemsService = orderItemsService;
}
@Override
public void onMessage(Message message, byte[] bytes) {
RedisSerializer<?> serializer = redisTemplate.getValueSerializer();
String channel = String.valueOf(serializer.deserialize(message.getChannel()));
String body = String.valueOf(serializer.deserialize(message.getBody()));
//key过期监听
if(StrUtil.format("__keyevent@{}__:expired", redisConfigProperties.getDatabase()).equals(channel)){
//订单自动取消
if(body.contains(RedisConst.REDIS_ORDER_KEY_IS_PAY_0)) {
body = body.replace(RedisConst.REDIS_ORDER_KEY_IS_PAY_0, "");
String[] str = body.split(":");
String wxOrderId = str[1];
System.out.println(wxOrderId);
Orders orders = ordersService.queryById(Integer.valueOf(wxOrderId));
if(orders != null && orders.getStatus() == 0){//只有待支付的订单能取消
//TODO 订单取消 库存增加 减优惠券
// orderItemsService.toCancel(orders.getId());
ordersService.cancelOrder(orders.getId());
System.out.println("订单id:"+orders.getId()+"已删除");
}
}