一、订单结算页
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 @Setter
private String orderToken;
private Integer count;
private BigDecimal total;
private BigDecimal payPrice;
@Getter @Setter
Map<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
* 异步丢失
*/
@Override
public 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() {
@Override
public void run() {
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
//1.查询用户信息
List<MemberAddressVo> list = memberFeignService.getMemberReceiveAddress(memberOAuthVo.getId());
orderConfirmVo.setMemberAddressVos(list);
}
}, executor);
CompletableFuture<Void> orderRunAsync = CompletableFuture.runAsync(new Runnable() {
@Override
public void run() {
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
//2.查询商品信息
List<OrderItemVo> orderItemVos = cartFeignService.orderFeignCart(memberOAuthVo.getId());
orderConfirmVo.setItems(orderItemVos);
}
}, executor).thenRunAsync(new Runnable() {
@Override
public void run() {
//查库存 怎么查库存 查库存参数List<Long> skuIds
List<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> valueMapper
Map<Long, Boolean> collect = data.stream().collect(Collectors.toMap(new Function<SkuHasStockVo, Long>() {
@Override
public 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 token
orderConfirmVo.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
@Override
public 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();
//订单号-唯一id
String 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
@Override
public 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;
}