订单服务笔记:
file:///E:/%E6%A1%8C%E9%9D%A2/%E8%B0%B7%E7%B2%92%E5%95%86%E5%9F%8E/%E8%B0%B7%E7%B2%92%E5%95%86%E5%9F%8E/%E9%AB%98%E7%BA%A7%E7%AF%87/%E8%AF%BE%E4%BB%B6/09%E3%80%81%E5%95%86%E5%9F%8E%E4%B8%9A%E5%8A%A1.pdf
接口幂等性笔记:
file:///E:/%E6%A1%8C%E9%9D%A2/%E8%B0%B7%E7%B2%92%E5%95%86%E5%9F%8E/%E8%B0%B7%E7%B2%92%E5%95%86%E5%9F%8E/%E9%AB%98%E7%BA%A7%E7%AF%87/%E8%AF%BE%E4%BB%B6/02%E3%80%81%E6%8E%A5%E5%8F%A3%E5%B9%82%E7%AD%89%E6%80%A7.pdf
接口幂等性笔记 百度云地址:https://pan.baidu.com/s/1uROLq_cna7IaRyxtx23C-A ,密码1111
订单服务
一、订单业务
1.结算页-去结算
1.1.页面cartList.html
<button onclick="toTrade()" type="button">去结算</button>
function toTrade() {
window.location.href = "http://order.gulimall.com/toTrade";
}
1.2.拦截器-订单所有请求必须登录后才可以使用
1).拦截器类-LoginUserInterceptor
/**
* @Description: 用户登录拦截器
**/
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
String uri = request.getRequestURI();
AntPathMatcher antPathMatcher = new AntPathMatcher();
boolean match = antPathMatcher.match("/order/order/status/**", uri);
boolean match1 = antPathMatcher.match("/payed/notify", uri);
if (match || match1) {
return true;
}
//获取登录的用户信息
MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute != null) {
//把登录后用户的信息放在ThreadLocal里面进行保存
loginUser.set(attribute);
return true;
} else {
//未登录,返回登录页面
// response.setContentType("text/html;charset=UTF-8");
// PrintWriter out = response.getWriter();
// out.println("<script>alert('请先进行登录,再进行后续操作!');location.href='http://auth.gulimall.com/login.html'</script>");
session.setAttribute("msg", "请先进行登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
2).使用拦截器-OrderWebConfig
/**
* @Description: web服务使用拦截器
**/
@Configuration
public class OrderWebConfig implements WebMvcConfigurer {
@Autowired
LoginUserInterceptor loginUserInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
}
}
1.3.order的OrderWebController
点击去结算,先登录,toTrade请求后,跳到订单的确认页confirm.html
1).OrderWebController
@Controller
public class OrderWebController {
@Autowired
OrderService orderService;
//结算
@GetMapping("/toTrade")
public String toTrade(Model model){
//展示订单确认的数据
OrderConfirmVo orderConfirmVo= orderService.confirmOrder();
System.out.println("订单确认的数据: "+ orderConfirmVo);
model.addAttribute("orderConfirmData",orderConfirmVo);
return "confirm";
}
}
2).OrderServiceImpl
//订单确认页返回的数据
@Override
public OrderConfirmVo confirmOrder() {
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
//1.根据memberId 远程查询所有收货地址列表--member
List<MemberAddressVo> memberAddress = memberFeignService.getAddress(memberRespVo.getId());
orderConfirmVo.setAddress(memberAddress);
//2.远程查询购物车选中的购物项-cart
List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
orderConfirmVo.setItems(currentCartItems);
//feign在远程调用之前,调用很多拦截器。远程调用,请求头丢失,使用feign拦截器
//3.查询用户积分
orderConfirmVo.setIntegration(memberRespVo.getIntegration());
//4.其他数据自动计算
//TODO 5. 防重令牌 。。幂等性再说
return orderConfirmVo;
}
问题一:feign远程调用 请求头丢失的问题
场景:订单服务远程调用购物车,没数据
解决方法:添加feign拦截器
@Configuration
public class GuliFeignConfig {
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor() {
RequestInterceptor requestInterceptor = new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
//1、使用RequestContextHolder拿到刚进来的请求数据
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
//老请求
HttpServletRequest request = requestAttributes.getRequest();
//2、同步请求头的数据(主要是cookie)
if (request != null) {
String cookie = request.getHeader("Cookie");
//给新请求同步了老请求的cookie值
template.header("Cookie", cookie);
}
}
}
};
return requestInterceptor;
}
}
3).对订单的结算方法confirmOrder进行异步编排,实现同一线程与异步
问题二:feign异步情况丢失上下文问题
解决方法:每个异步任务,设置老请求
修改后:confirmOrder方法
//订单确认页返回的数据
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//1.根据memberId 远程查询所有收货地址列表--member
CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
List<MemberAddressVo> memberAddress = memberFeignService.getAddress(memberRespVo.getId());
orderConfirmVo.setAddress(memberAddress);
}, executor);
//2.远程查询购物车选中的购物项-cart
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
orderConfirmVo.setItems(currentCartItems);
//feign在远程调用之前,调用很多拦截器。远程调用,请求头丢失,使用feign拦截器
}, executor);
//3.查询用户积分
orderConfirmVo.setIntegration(memberRespVo.getIntegration());
//4.其他数据自动计算
//TODO 5. 防重令牌 。。幂等性再说
CompletableFuture.allOf(addressFuture,cartFuture).get();
return orderConfirmVo;
}
4).添加库存
//2.远程查询购物车选中的购物项-cart
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
orderConfirmVo.setItems(currentCartItems);
//feign在远程调用之前,调用很多拦截器。远程调用,请求头丢失,使用feign拦截器
}, executor).thenRunAsync(()->{
List<OrderItemVo> items = orderConfirmVo.getItems();
List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
R r = wmsFeignService.getSkuHasStock(collect);
List<SkuStockVo> data = r.getData(new TypeReference<List<SkuStockVo>>() {});
if (data!=null){
Map<Long, Boolean> map = data.stream().
collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
orderConfirmVo.setStocks(map);
}
});
5).选择收货人
逻辑:点击某个收货,把它高亮,且把他设为1,根据addrId获取运费
操作js
库存ware的方法 WareInfoController与实现类
@GetMapping(value = "/fare")
public R getFare(@RequestParam("addrId") Long addrId) {
FareVo fare = wareInfoService.getFare(addrId);
return R.ok().setData(fare);
}
//查询运费和收货地址信息
@Override
public FareVo getFare(Long addrId) {
FareVo fareVo = new FareVo();
R r = memberFeignService.info(addrId);
MemberAddressVo data = r.getData("memberReceiveAddress",new TypeReference<MemberAddressVo>(){});
if (data!=null){
//todo 设置运费。此处截取下手机最后移位作为运费
String phone = data.getPhone();
String substring = phone.substring(phone.length() - 1, phone.length());
System.out.println("地址信息data:"+data);
fareVo.setAddress(data);
fareVo.setFare(new BigDecimal(substring));
return fareVo;
}
return null;
}
2.购物车-提交订单
2.1.需求:不能重复提交订单,接口幂等性
接口幂等性笔记:
百度云接口幂等性笔记,密码1111
本地接口幂等性笔记:
file:///E:/%E6%A1%8C%E9%9D%A2/%E8%B0%B7%E7%B2%92%E5%95%86%E5%9F%8E/%E8%B0%B7%E7%B2%92%E5%95%86%E5%9F%8E/%E9%AB%98%E7%BA%A7%E7%AF%87/%E8%AF%BE%E4%BB%B6/02%E3%80%81%E6%8E%A5%E5%8F%A3%E5%B9%82%E7%AD%89%E6%80%A7.pdf
2.2. 幂等性介绍
2.2.1.什么是幂等性
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用;
比如说支付场景,用户购买了商品支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条...,这就没有保证接口 的幂等性。
2.2.2.哪些情况需要防止
用户多次点击按钮
用户页面回退再次提交
微服务互相调用,由于网络问题,导致请求失败。feign 触发重试机制
其他业务情况
2.2.3.什么情况下需要幂等
以 SQL 为例,有些操作是天然幂等的。 SELECT * FROM table WHER id=?,无论执行多少次都不会改变状态,是天然的幂等。
UPDATE tab1 SET col1=1 WHERE col2=2,无论执行成功多少次状态都是一致的,也是幂等操作。
delete from user where userid=1,多次操作,结果一样,具备幂等性
insert into user(userid,name) values(1,‘a’) 如 userid 为唯一主键,即重复操作上面的业务,只 会插入一条用户数据,具备幂等性。
UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次执行的结果都会发生变化,不是幂等的。
insert into user(userid,name) values(1,‘a’) 如 userid 不是主键,可以重复,那上面业务多次操 作,数据都会新增多条,不具备幂等性。
2.2.4.幂等解决方案–看笔记
接口幂等性笔记 百度云地址:https://pan.baidu.com/s/1uROLq_cna7IaRyxtx23C-A ,密码1111
1).token 机制
2).各种锁机制
3).各种唯一约束
4).防重表
5).全局请求唯一 id
2.3 提交订单功能
2.3.1 controller
//2.提交订单
@PostMapping("/submitOrder")
public String submitOrder(OrderSubmitVo vo,Model model,RedirectAttributes redirectAttributes) {
SubmitOrderResponseVo responseVo = orderService.submitOrder(vo);
//下单:去创建订单,验令牌,验库存...
if (responseVo.getCode() == 0) {
//下单成功,来到支付选择业
System.out.println("订单提交成功");
model.addAttribute("submitOrderResp",responseVo);
return "pay";
} else {
//下单失败,回到订单确认页重新确认订单
System.out.println("订单提交失败。。。");
String msg="订单提交失败";
switch (responseVo.getCode()){
case 1: msg+=",订单信息过期,请刷新再次提交";break;
case 2: msg+=",订单商品价格发生变化,请确认后再次提交";break;
case 3: msg+=",库存锁定失败,商品库存不足";break;
}
redirectAttributes.addFlashAttribute("msg",msg);
return "redirect:http://order.gulimall.com/toTrade";
}
}
2.3.2 实现类OrderServiceImpl
1).submitOrder
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
//把OrderSubmitVo的数据放入ThreadLocal,进行数据共享
confirmVoThreadLocal.set(vo);
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
response.setCode(0);
//下单:去创建订单,验令牌,验库存...
//1.验证令牌 [令牌的对比与删除必须原子性]
// (redis与页面的token比较)
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//0令牌失败,1删除成功
String orderToken = vo.getOrderToken();
//通过lure脚本原子验证令牌和删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
//令牌验证(幂等性验证)
if (result == 0L) {
//令牌验证失败
response.setCode(1);
return response;
} else {
//令牌成功。删除redis的orderToken
//下单:去创建订单,验令牌,验价格、验库存...
response.setCode(0);
//1.创建订单、订单项等信息
OrderCreateTo order = createOrder();
//2.验价(应付价格)
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = vo.getPayPrice();
//两个价格相减的绝对值小于0.01,表示相同
if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
//金额对比成功...
//3.保存订单
saveOrder(order);
//4.库存锁定。只要有异常,回滚订单数据。
// 远程ware的wms_ware_sku。订单号、所有订单项(skuId、skuName、num)
WareSkuLockVo lockVo = new WareSkuLockVo();
lockVo.setOrderSn(order.getOrder().getOrderSn());
//获取要锁定的商品数据信息
List<OrderItemVo> orderItemVos = order.getOrderItems().stream().map((item) -> {
OrderItemVo itemVo = new OrderItemVo();
itemVo.setSkuId(item.getSkuId());
itemVo.setCount(item.getSkuQuantity());
itemVo.setTitle(item.getSkuName());
return itemVo;
}).collect(Collectors.toList());
lockVo.setLocks(orderItemVos);
//todo 远程锁定库存 ware , ok啦
R r = wmsFeignService.orderLockStock(lockVo);
if (r.getCode() == 0) {
//锁定库存成功
response.setOrder(order.getOrder());
return response;
} else {
//锁定库存失败
response.setCode(3);
throw new NoStockException();
}
} else {
//价格对比失败
response.setCode(2);
return response;
}
}
}
2).saveOrder-保存订单数据
//保存订单数据
private void saveOrder(OrderCreateTo orderCreateTo) {
//获取订单信息,保存订单
OrderEntity order = orderCreateTo.getOrder();
order.setCreateTime(new Date());
order.setModifyTime(new Date());
this.save(order);
//获取订单项信息,批量保存
List<OrderItemEntity> orderItems = orderCreateTo.getOrderItems();
orderItemService.saveBatch(orderItems);
}
3).createOrder-创建订单
//创建订单
private OrderCreateTo createOrder() {
OrderCreateTo createTo = new OrderCreateTo();
//1.创建订单号
String orderSn = IdWorker.getTimeId();
OrderEntity orderEntity = buildOrder(orderSn);
//2.获取所有的订单项
List<OrderItemEntity> orderItemEntities = buildOrderItems(orderSn);
//3.计算价格、积分信息等
computePrice(orderEntity, orderItemEntities);
//把创建的订单、订单项 放入创建订单的实体类
createTo.setOrder(orderEntity);
createTo.setOrderItems(orderItemEntities);
return createTo;
}
4). computePrice-计算价格
//计算价格
private void computePrice(OrderEntity orderEntity, List<OrderItemEntity> orderItemEntities) {
//总价
BigDecimal total = new BigDecimal("0.0");
//优惠价
BigDecimal coupon = new BigDecimal("0.0");
BigDecimal promotion = new BigDecimal("0.0");
BigDecimal integration = new BigDecimal("0.0");
//积分、成长值
BigDecimal giftGrowth = new BigDecimal("0.0");
BigDecimal giftIntegration = new BigDecimal("0.0");
//订单总额:每个订单的总额之和
for (OrderItemEntity item : orderItemEntities) {
//优惠、积分、促销、总额
coupon = coupon.add(item.getCouponAmount());
promotion = promotion.add(item.getPromotionAmount());
integration = integration.add(item.getIntegrationAmount());
total = total.add(item.getRealAmount());
//赠送积分、成长值
giftGrowth.add(new BigDecimal(item.getGiftGrowth().toString()));
giftIntegration.add(new BigDecimal(item.getGiftIntegration().toString()));
}
//1.价格有关的
orderEntity.setTotalAmount(total);
//应付总额=总额+运费
orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount()));
//设置优惠、积分、促销的价格金额
orderEntity.setCouponAmount(coupon);
orderEntity.setPromotionAmount(promotion);
orderEntity.setIntegrationAmount(integration);
//设置赠送的积分、成长值
orderEntity.setGrowth(giftGrowth.intValue());
orderEntity.setIntegration(giftIntegration.intValue());
orderEntity.setDeleteStatus(0);//0 未删除
}```
##### 5). buildOrder-构建订单
```java
//构建订单
private OrderEntity buildOrder(String orderSn) {
//获取当前用户登录信息
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(orderSn);//订单号
orderEntity.setMemberId(memberRespVo.getId());
orderEntity.setMemberUsername(memberRespVo.getUsername());
OrderSubmitVo orderSubmitVo = confirmVoThreadLocal.get();
//远程ware获取收货地址、运费信息
R r = wmsFeignService.getFare(orderSubmitVo.getAddrId());
FareVo fareData = r.getData("data",new TypeReference<FareVo>() {});
orderEntity.setFreightAmount(fareData.getFare());//运费
MemberAddressVo address = fareData.getAddress();
//设置收货信息
orderEntity.setReceiverName(address.getName());
orderEntity.setReceiverPhone(address.getPhone());
orderEntity.setReceiverPostCode(address.getPostCode());
orderEntity.setReceiverProvince(address.getProvince());
orderEntity.setReceiverCity(address.getCity());
orderEntity.setReceiverRegion(address.getRegion());
orderEntity.setReceiverDetailAddress(address.getDetailAddress());
//设置订单相关的状态信息
orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
orderEntity.setConfirmStatus(7);//自动收货
return orderEntity;
}
6).buildOrderItems-构建所有订单项的数据
//构建所有订单项数据
private List<OrderItemEntity> buildOrderItems(String orderSn) {
//最后一次确定每个购物项的价格
List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
if (currentCartItems != null && currentCartItems.size() > 0) {
List<OrderItemEntity> itemEntities = currentCartItems.stream().map((cartItem) -> {
OrderItemEntity orderItem = buildOrderItem(cartItem);
orderItem.setOrderSn(orderSn);
return orderItem;
}).collect(Collectors.toList());
return itemEntities;
} else {
return null;
}
}
7).buildOrderItem-构建某个订单项的数据
//构建某个订单项的数据
private OrderItemEntity buildOrderItem(OrderItemVo cartItem) {
OrderItemEntity orderItem = new OrderItemEntity();
//订单信息,订单号 v
//1.商品Spu信息
Long skuId = cartItem.getSkuId();
//获取spu的信息
R r = productFeignService.getSpuInfoBySkuId(skuId);
SpuInfoVo spuInfo = r.getData(new TypeReference<SpuInfoVo>() {});
orderItem.setSpuId(spuInfo.getId());
orderItem.setSpuName(spuInfo.getSpuName());
orderItem.setCategoryId(spuInfo.getCatalogId());
orderItem.setSpuBrand(spuInfo.getBrandName());
//2.商品Sku信息
orderItem.setSkuId(cartItem.getSkuId());
orderItem.setSkuName(cartItem.getTitle());
orderItem.setSkuPic(cartItem.getImage());
orderItem.setSkuPrice(cartItem.getPrice());
orderItem.setSkuQuantity(cartItem.getCount());
//设置属性,list集合变成String
String skuAttr = StringUtils.collectionToDelimitedString(cartItem.getSkuAttr(), ";");
orderItem.setSkuAttrsVals(skuAttr);
//3.优惠信息(不做)
//4.积分信息
orderItem.setGiftIntegration(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
orderItem.setGiftGrowth(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
//5.订单项的价格信息
//todo 远程查询优惠信息,此处设为 0
orderItem.setPromotionAmount(new BigDecimal("0"));
orderItem.setIntegrationAmount(BigDecimal.ZERO);
orderItem.setCouponAmount(BigDecimal.ZERO);
//当前订单项的实际金额
BigDecimal origin = orderItem.getSkuPrice().multiply(new BigDecimal(orderItem.getSkuQuantity().toString()));
BigDecimal subtract = origin.subtract(orderItem.getPromotionAmount())
.subtract(orderItem.getIntegrationAmount())
.subtract(orderItem.getCouponAmount());
orderItem.setRealAmount(subtract);
return orderItem;
}