最近在写的支付模块

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

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()+"已删除");
                }
            }
相关推荐
苹果醋33 分钟前
React源码02 - 基础知识 React API 一览
java·运维·spring boot·mysql·nginx
某柚啊19 分钟前
Windows开启IIS后依然出现http error 503.the service is unavailable
windows·http
Hello.Reader23 分钟前
深入解析 Apache APISIX
java·apache
菠萝蚊鸭44 分钟前
Dhatim FastExcel 读写 Excel 文件
java·excel·fastexcel
码农君莫笑1 小时前
信管通低代码信息管理系统应用平台
linux·数据库·windows·低代码·c#·.net·visual studio
旭东怪1 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
007php0071 小时前
Go语言zero项目部署后启动失败问题分析与解决
java·服务器·网络·python·golang·php·ai编程
∝请叫*我简单先生1 小时前
java如何使用poi-tl在word模板里渲染多张图片
java·后端·poi-tl
ssr——ssss1 小时前
SSM-期末项目 - 基于SSM的宠物信息管理系统
java·ssm
一棵星1 小时前
Java模拟Mqtt客户端连接Mqtt Broker
java·开发语言