最近在写的支付模块

最近再写支付模块就到处借鉴 旨在回顾一下。

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()+"已删除");
                }
            }
相关推荐
YHPsophie8 分钟前
ATGM331C-5T杭州中科微BDS/GNSS全星座定位授时模块应用领域
经验分享·笔记·单片机·信息与通信·交通物流
程序员大金11 分钟前
基于SpringBoot+Vue+MySQL的智能物流管理系统
java·javascript·vue.js·spring boot·后端·mysql·mybatis
周伯通*17 分钟前
Windows上,使用远程桌面连接Ubuntu
linux·windows·ubuntu
月夕花晨37422 分钟前
C++学习笔记(30)
c++·笔记·学习
茜茜西西CeCe27 分钟前
移动技术开发:登录注册界面
java·gitee·gradle·android studio·安卓·移动技术开发·原生安卓开发
linux_lzj_cainiao27 分钟前
准备招银社招记录
java
不是编程家32 分钟前
C++ 第三讲:内存管理
java·开发语言·c++
尸僵打怪兽33 分钟前
软考(中级-软件设计师)(0919)
java·c语言·数据库·计算机网络·软考·多媒体·软件设计师
Liii40343 分钟前
【ARM】Cache深度解读
java·arm开发·spring
litGrey1 小时前
Maven国内镜像(四种)
java·数据库·maven