一、订单结算页
1.业务分析
(1) 获取用户收货地址信息
一般的收货地址都是多个,使用时选中一个,所以收货地址使用List集合封装
(2)获取购物车商品信息
购物车商品也是多个,使用List集合封装
(3)查询商品库存
查询每个商品是否有库存,显示是否有货,使用Map封装
(4)计算订单金额
使用BigDecimal计算,算出每个商品的总价(价格*件数)然后求和,最后扣除优惠价格得到结算价,还可以做积分业务
(5)页面防重复提交,保证幂等性
重复提交会重复执行下单业务,提交订单前在redis中存一个token令牌,然后在执行下单操作前去redis中去取这个token,如果能取到,代表是第一次提交页面,然后删除令牌,如果取不到证明令牌代表已经被删除了,表示页面不是第一次提交,告诉用户 “请问重复提交页面”
删令牌的操作需要保证原子性,取令牌,校验,删除是三次操作,如果其中一步错误就会导致删除失败,所以必须保证要么全部成功或全部失败,这就是原子性,所以使用redis提供的lua脚本取执行删令牌的操作
2.代码
(1)订单页面实体类设计
public class OrderConfirmVo {@Getter @Setter/** 会员收获地址列表 **/List<MemberAddressVo> memberAddressVos;@Getter @Setter/** 所有选中的购物项 **/List<OrderItemVo> items;/** 发票记录 **/@Getter @Setter/** 优惠券(会员积分) **/private Integer integration;/** 防止重复提交的令牌 **/@Getter @Setterprivate String orderToken;private Integer count;private BigDecimal total;private BigDecimal payPrice;@Getter @SetterMap<Long,Boolean> stocks;/*** 获取商品总数量*/public Integer getCount() {Integer count = 0;if (items != null && items.size() > 0) {for (OrderItemVo item : items) {count += item.getCount();}}return count;}/** 订单总额 **///BigDecimal total;//计算订单总额public BigDecimal getTotal() {BigDecimal totalNum = BigDecimal.ZERO;for (OrderItemVo item : items) {BigDecimal multiply = item.getPrice().multiply(new BigDecimal(Integer.toString(item.getCount())));totalNum = totalNum.add(multiply);}return totalNum;}/** 应付价格 **///BigDecimal payPrice;public BigDecimal getPayPrice() {return getTotal();}
}
(1)业务使用异步编排提升效率
/*** 展示结算页信息** Feign 丢失请求头的问题? 如果使用ThreadLocal的方式直接获取id会获取不到,远程调用没经过拦截器获取不到id* 异步丢失*/@Overridepublic OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {OrderConfirmVo orderConfirmVo = new OrderConfirmVo();MemberOAuthVo memberOAuthVo = OrderLoginIntercepter.threadLocal.get();//TODO :获取当前线程请求头信息(解决Feign异步调用丢失请求头问题)RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();CompletableFuture<Void> memberRunAsync = CompletableFuture.runAsync(new Runnable() {@Overridepublic void run() {//每一个线程都来共享之前的请求数据RequestContextHolder.setRequestAttributes(requestAttributes);//1.查询用户信息List<MemberAddressVo> list = memberFeignService.getMemberReceiveAddress(memberOAuthVo.getId());orderConfirmVo.setMemberAddressVos(list);}}, executor);CompletableFuture<Void> orderRunAsync = CompletableFuture.runAsync(new Runnable() {@Overridepublic void run() {//每一个线程都来共享之前的请求数据RequestContextHolder.setRequestAttributes(requestAttributes);//2.查询商品信息List<OrderItemVo> orderItemVos = cartFeignService.orderFeignCart(memberOAuthVo.getId());orderConfirmVo.setItems(orderItemVos);}}, executor).thenRunAsync(new Runnable() {@Overridepublic void run() {//查库存 怎么查库存 查库存参数List<Long> skuIdsList<Long> skuIds = orderConfirmVo.getItems().stream().map(item -> {return item.getSkuId();}).collect(Collectors.toList());R<List<SkuHasStockVo>> r = wareFeignService.getSkuHasStock(skuIds);if (r.getCode() == 0){List<SkuHasStockVo> data = r.getData(new TypeReference<List<SkuHasStockVo>>() {});// 下界 接收参数的类型,上界 返回的类型//Function<? super T, ? extends K> keyMapper,//Function<? super T, ? extends U> valueMapperMap<Long, Boolean> collect = data.stream().collect(Collectors.toMap(new Function<SkuHasStockVo, Long>() {@Overridepublic Long apply(SkuHasStockVo skuHasStockVo) {return skuHasStockVo.getSkuId();}}, SkuHasStockVo::getHasStock));orderConfirmVo.setStocks(collect);}}});//会员积分orderConfirmVo.setIntegration(memberOAuthVo.getIntegration());//设置订单token,防止页面重复提交,保证幂等性String key = OrderConstant.ORDER_TOKEN_PREFIX + memberOAuthVo.getId();String orderToken = UUID.randomUUID().toString().replace("_","");redisTemplate.opsForValue().set(key,orderToken) ;//set tokenorderConfirmVo.setOrderToken(orderToken);CompletableFuture.allOf(memberRunAsync,orderRunAsync).get();return orderConfirmVo;}
二、下单
1.业务分析
(1)校验页面幂等性
获取token令牌,删除令牌,校验成功进入下单操作,校验失败返回code
(2)创建订单
1.生成订单号,使用唯一ID生成,存入OrderSn
2.获取用户选中的地址信息,存入OrderEntity
3.获取购物车最新的商品信息,这里不能直接获取订单页的商品信息,防止购物车商品信息发生变化与订单页商品不一致
根据上送skuId取获取redis中存储的商品信息,然后筛选出已勾选的商品,并且查出该商品最新价格,最后存入List
(3) 验价
防止商品价格发生变化(可能订单页保存的价格与最新价格存在差异),在下单前进行比对,这里使用两价格相减取绝对值,考虑到存在优惠的问题,只要保证绝对值<0.05就表示验价通过
(4)保存订单数据
验价成功就可以保证订单,但需要添加事务@Transactional
(5)锁定库存
商品下单需要锁定库存,相当于订一批货,那么别人在去买这批货时就无法购买,先占个位置,如果超过支付时间未付款,就解锁库存,锁库存操作是给锁定库存添加需要购买的件数stock_locked + num,最大不能超过商品总库存 stock - stock_locked >= num
锁库存先查该skuId商品在那些仓库有库存,然后循环去锁库存,只要有一个仓库锁库存成功就代表成功,如果没有一个仓库能锁成功代表锁库存失败,告诉用户该商品库存不足,并且锁库存之前需要判断是否有仓库存在库存,没有就直接不用锁库存了
2.代码
(1)下单整体业务
@Transactional@Overridepublic SubmitOrderResponseVo placeOrder(OrderSubmitVo submitVo) {SubmitOrderResponseVo submitOrderResponseVo = new SubmitOrderResponseVo();submitOrderResponseVo.setCode(0);MemberOAuthVo memberOAuthVo = OrderLoginIntercepter.threadLocal.get();String key = OrderConstant.ORDER_TOKEN_PREFIX + memberOAuthVo.getId();//原子性操作:1.取出token 2.对比token 3.删除token//execute(RedisScript<T> script, List<K> keys, Object... args)String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";Long luaReturn = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(key), submitVo.getOrderToken());//1-删除成功 0-删除失败if (luaReturn.equals(1L)){//令牌删除成功 -> 创建订单OrderCreateTo order = createOrder(submitVo);//验价 应付价格-BigDecimal payAmount = order.getOrder().getPayAmount();//应付价格BigDecimal payPrice = submitVo.getPayPrice();double abs = Math.abs(payAmount.subtract(payPrice).doubleValue());if (abs < 0.01){//验价成功 -> 保存订单信息 -> 锁定库存//TODO 保存订单信息saveOrder(order.getOrder(),order.getOrderItems());//远程锁定库存List<OrderItemEntity> orderItems = order.getOrderItems();List<OrderItemVo> collect = orderItems.stream().map(item -> {OrderItemVo orderItemVo = new OrderItemVo();orderItemVo.setSkuId(item.getSkuId());orderItemVo.setCount(item.getSkuQuantity());return orderItemVo;}).collect(Collectors.toList());OrderStockRequest orderStockRequest = new OrderStockRequest();orderStockRequest.setOrderSn(order.getOrder().getOrderSn());orderStockRequest.setItemVos(collect);//TODO 远程锁库存R r = wareFeignService.orderStock(orderStockRequest);if (r.getCode() == 0){submitOrderResponseVo.setCode(0);submitOrderResponseVo.setOrder(order.getOrder());return submitOrderResponseVo;}else {//锁库存失败//锁定失败String msg = (String) r.get("msg");throw new SkuNoStockException(msg);}}else {//验价失败submitOrderResponseVo.setCode(2);return submitOrderResponseVo;}}else {//令牌删除失败submitOrderResponseVo.setCode(1);return submitOrderResponseVo;}}
(2)获取订单页信息
/*** 创建订单* (1)用户信息:地址 1* (2)购物车商品信息:应该直接查询购物车数据,不能获取结算页的商品数据, list* (3)下单金额 1* (4) 验价*/public OrderCreateTo createOrder(OrderSubmitVo submitVo){OrderCreateTo orderCreateTo = new OrderCreateTo();//订单号-唯一idString timeId = IdWorker.getTimeId();OrderEntity orderEntity = buildOrderEntity(submitVo, timeId);List<OrderItemEntity> orderItemEntityList = buildOrderItems(timeId);//封装:订单总额、应付总额OrderEntity order = computePrice(orderEntity,orderItemEntityList);orderCreateTo.setOrder(order);orderCreateTo.setOrderItems(orderItemEntityList);return orderCreateTo;}/*** 封装OrderEntity:* 订单总额、应付总额* 促销优化金额、积分抵扣金额、优惠券抵扣金额**/public OrderEntity computePrice(OrderEntity order,List<OrderItemEntity> orderItems){//订单总额BigDecimal totalAmount = new BigDecimal("0.0");//促销优化金额、积分抵扣金额、优惠券抵扣金额BigDecimal promotionAmount = new BigDecimal("0.0");BigDecimal integrationAmount = new BigDecimal("0.0");BigDecimal couponAmount = new BigDecimal("0.0");//积分、成长值Integer integration = 0;Integer growth = 0;for (OrderItemEntity orderItem : orderItems) {totalAmount = totalAmount.add(orderItem.getRealAmount());promotionAmount = promotionAmount.add(orderItem.getPromotionAmount());integrationAmount = integrationAmount.add(orderItem.getIntegrationAmount());couponAmount = couponAmount.add(orderItem.getCouponAmount());integration += orderItem.getGiftIntegration();growth += orderItem.getGiftGrowth();}//订单总额order.setTotalAmount(totalAmount);//应付总额 = 订单总额 + 运费order.setPayAmount(totalAmount.add(order.getFreightAmount()));//促销优化金额、积分抵扣金额、优惠券抵扣金额order.setPromotionAmount(promotionAmount);order.setIntegrationAmount(integrationAmount);order.setCouponAmount(couponAmount);//积分、成长值order.setIntegration(integration);order.setGrowth(growth);return order;}/*** 订单信息* OrderEntity** 1.订单号* 2.用户地址信息* 3.运费金额*/public OrderEntity buildOrderEntity(OrderSubmitVo submitVo,String timeId){OrderEntity orderEntity = new OrderEntity();orderEntity.setOrderSn(timeId);orderEntity.setCreateTime(new Date());//用户地址MemberOAuthVo memberOAuthVo = OrderLoginIntercepter.threadLocal.get();orderEntity.setMemberId(memberOAuthVo.getId());MemberAddressVo member = memberFeignService.getMember(memberOAuthVo.getId());orderEntity.setReceiverName(member.getName());orderEntity.setReceiverPhone(member.getPhone());orderEntity.setReceiverPostCode(member.getPostCode());orderEntity.setReceiverProvince(member.getProvince());orderEntity.setReceiverCity(member.getCity());orderEntity.setReceiverRegion(member.getRegion());orderEntity.setReceiverDetailAddress(member.getDetailAddress());//用户名orderEntity.setMemberUsername(memberOAuthVo.getUsername());//运费金额R r = wareFeignService.fare(submitVo.getAddrId());if (r.getCode() == 0){FareVo data = (FareVo)r.getData(new TypeReference<FareVo>() {});//运费金额orderEntity.setFreightAmount(data.getFare());}orderEntity.setPayType(submitVo.getPayType());//订单状态-0待付款orderEntity.setStatus(OrderConstant.ORDER_STATUS);//字段确认天数orderEntity.setAutoConfirmDay(7);return orderEntity;}/*** 订单商品* List<OrderItemEntity> 因为商品有多个,所以是list** 1.订单号* 2.sku信息* 3.spu信息*/public List<OrderItemEntity> buildOrderItems(String timeId){//查询redis商品信息MemberOAuthVo memberOAuthVo = OrderLoginIntercepter.threadLocal.get();List<OrderItemVo> orderItemVos = cartFeignService.orderFeignCart(memberOAuthVo.getId());List<OrderItemEntity> collect = orderItemVos.stream().map(item -> {OrderItemEntity orderItemEntity = new OrderItemEntity();//订单号orderItemEntity.setOrderSn(timeId);//spu信息SpuInfoEntity spuInfo = productFeignService.getSpuBySkuId(item.getSkuId());orderItemEntity.setSpuId(spuInfo.getId());orderItemEntity.setSpuName(spuInfo.getSpuName());orderItemEntity.setCategoryId(spuInfo.getCatalogId());//Sku信息orderItemEntity.setSkuId(item.getSkuId());orderItemEntity.setSkuName(item.getTitle());orderItemEntity.setSkuPic(item.getImage());orderItemEntity.setSkuPrice(item.getPrice());orderItemEntity.setSkuQuantity(item.getCount());//商品销售属性组合 List<String> -> String//集合 根据指定的分割符转换 为字符串String jsonString = JSON.toJSONString(item.getSkuAttrValues());orderItemEntity.setSkuAttrsVals(jsonString);//促销、优惠券、积分orderItemEntity.setPromotionAmount(BigDecimal.ZERO);orderItemEntity.setCouponAmount(BigDecimal.ZERO);orderItemEntity.setIntegrationAmount(BigDecimal.ZERO);//该商品经过优惠后的分解金额BigDecimal totalPrice = orderItemEntity.getSkuPrice().multiply(new BigDecimal(orderItemEntity.getSkuQuantity().toString()));BigDecimal realPrice = totalPrice.subtract(orderItemEntity.getPromotionAmount()).subtract(orderItemEntity.getCouponAmount()).subtract(orderItemEntity.getIntegrationAmount());orderItemEntity.setRealAmount(realPrice);//赠送积分、成长值orderItemEntity.setGiftIntegration(totalPrice.intValue());orderItemEntity.setGiftGrowth(totalPrice.intValue());return orderItemEntity;}).collect(Collectors.toList());return collect;}
(3)锁定库存
/*** 库存锁定** 1.查询该商品在那些仓库有库存* 2.锁定库存,遍历仓库去锁定库存,只要有一个仓库锁定代表成功,如果没有一个仓库能锁成功,抛异常,该sku商品库存不足**/
// @Transactional(rollbackFor = NoWareStockException.class)@Transactional@Overridepublic boolean orderStock(OrderStockRequest orderStockRequest) {List<OrderItemVo> itemVos = orderStockRequest.getItemVos();List<SkuStockfromWare> collect = itemVos.stream().map(item -> {SkuStockfromWare skuStockfromWare = new SkuStockfromWare();skuStockfromWare.setSkuId(item.getSkuId());skuStockfromWare.setNum(item.getCount());//查询该商品在那些仓库有库存List<Long> wareId = wareSkuDao.skuStockfromWare(item.getSkuId());skuStockfromWare.setWareId(wareId);return skuStockfromWare;}).collect(Collectors.toList());//根据skuId遍历for (SkuStockfromWare skuStockfromWare : collect) {//判断是否锁定成功boolean flag = false;//判断该商品是否有仓库存在库存List<Long> wareIdList = skuStockfromWare.getWareId();if (wareIdList.size() < 0 || wareIdList == null){throw new NoWareStockException(skuStockfromWare.getSkuId());}for (Long wareId : wareIdList) {Long count = wareSkuDao.LockedStockFromWare(skuStockfromWare.getSkuId(),wareId,skuStockfromWare.getNum());if (count.equals(1L)){//锁定成功flag = true;//该商品锁定库存成功就执行下一个商品break;}}//如果没有一个仓库扣成功,代表此skuId的库存不足if (!flag){throw new SkuNoStockException(skuStockfromWare.getSkuId());}}return true;}