导航:
谷粒商城笔记+踩坑汇总篇
目录
1、订单确认页
1.1、vo类抽取
1.2、获取订单详情页数据,完整代码
1.2.1、Controller编写跳转订单确认页方法
1.2.2、Service获取订单详情页数据
1.3、【会员模块】获取会员所有收货地址
1.3.1、controller
1.3.2、service
1.4、订单服务远程调用用户服务
1.5、【购物车模块】 获取用户选择的所有CartItem
1.5.1、业务流程
1.5.2、编写Controller层接口
1.5.3、Service层实现类
1.5.4、【商品模块】获取指定商品的价格
1.5.5、购物车服务远程调用商品服务
1.5.6、订单服务远程调用购物车服务
1.6、Feign远程调用丢失请求头问题
1.6.1、问题分析
1.6.2、【订单模块】解决:配置类添加请求拦截器
1.7、异步线程丢失主线程请求头问题
1.8、前端,订单确认页渲染
1.9、订单确认页里,商品的库存查询
1.10、根据用户地址ID,返回详细地址并计算物流费
1.10.1、需求
1.10.2、前端,选择收货地址页面效果
1.10.3、 模型类抽取
1.10.4、controller
1.10.5、仓库模块远程调用用户模块,查地址信息
1.10.6、service,根据地址id获取地址信息和费用
1.11、保证接口幂等性,防重复提交表单
1.11.1、幂等性概述
1.11.2、任务幂等性的三种保证方法
1.11.3、业务流程
1.11.4、代码实现,防重复提交表单,唯一序列号方式保证幂等性
11.1.5 测试
1、订单确认页
1.1、vo类抽取
订单确认页需要用的数据
- 因为存在网路延迟等问题,若一直点下单会下许多。所以我们需要防重令牌
com.atguigu.gulimall.order.vo
/**
* Description: 订单确认页需要用的数据
*/
public class OrderConfirmVo {
/**
* 收货地址,ums_member_receive_address 表
*/
@Setter@Getter
List<MemberAddressVo> addressVos;
/**
* 所有选中的购物车项
*/
@Setter@Getter
List<OrderItemVo> items;
// 发票记录。。。
/**
* 优惠券信息
*/
@Setter@Getter
Integer integration;
/**
* 是否有库存
*/
@Setter@Getter
Map<Long,Boolean> stocks;
/**
* 防重令牌
*/
@Setter@Getter
String OrderToken;
/**
* @return 订单总额
* 所有选中商品项的价格 * 其数量
*/
public BigDecimal getTotal() {
BigDecimal sum = new BigDecimal("0");
if (items != null) {
for (OrderItemVo item : items) {
BigDecimal multiply = item.getPrice().multiply(new BigDecimal(item.getCount().toString()));
sum = sum.add(multiply);
}
}
return sum;
}
/**
* 应付价格
*/
//BigDecimal pryPrice;
public BigDecimal getPryPrice() {
return getTotal();
}
public Integer getCount(){
Integer i =0;
if (items!=null){
for (OrderItemVo item : items) {
i+=item.getCount();
}
}
return i;
}
}
收货地址,ums_member_receive_address 表
package com.atguigu.gulimall.order.vo;
@Data
public class OrderConfirmVo {
/**
* 收货地址,ums_member_receive_address 表
*/
List<MemberAddressVo> addressVos;
/**
* 所有选中的购物车项
*/
List<OrderItemVo> items;
// 发票记录。。。
/**
* 优惠券信息
*/
Integer integration;
/**
* 订单总额
*/
BigDecimal total;
/**
* 应付价格
*/
BigDecimal pryPrice;
}
商品项信息
package com.atguigu.gulimall.order.vo;
@Data
public class OrderItemVo {
/**
* 商品Id
*/
private Long skuId;
/**
* 商品标题
*/
private String title;
/**
* 商品图片
*/
private String image;
/**
* 商品套餐信
*/
private List<String> skuAttr;
/**
* 商品价格
*/
private BigDecimal price;
/**
* 数量
*/
private Integer count;
/**
* 小计价格
*/
private BigDecimal totalPrice;
}
1.2、获取订单详情页数据,完整代码
1.2.1、Controller编写跳转订单确认页方法
com.atguigu.gulimall.order.web
@Controller
public class OrderWebController {
@Autowired
OrderService orderService;
//去结算确认页
@GetMapping("/toTrade")
public String toTrade(Model model){
OrderConfirmVo confirmVo = orderService.confirmOrder();
model.addAttribute("OrderConfirmData",confirmVo);
return "confirm";
}
}
1.2.2、Service获取订单详情页数据
业务流程:
- 1、远程查询所有的地址列表
- 2、远程查询购物车所有选中的购物项
- 3、查询用户积分
- 4、其他数据自动计算
- 5、防重令牌
com.atguigu.gulimall.order.service.impl
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {
@Autowired
MemberFeignService memberFeignService;
@Autowired
CartFeignService cartFeignService;
/**
* 订单确认页返回需要用的数据
* @return
*/
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
//构建响应模型类OrderConfirmVo
OrderConfirmVo confirmVo = new OrderConfirmVo();
//从拦截器ThreadLocal获取当前用户登录的信息
MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
//TODO :获取当前线程请求头信息(解决Feign异步调用丢失请求头问题)
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//开启第一个异步任务
CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
//1、远程查询所有的收获地址列表
List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId());
confirmVo.setMemberAddressVos(address);
}, threadPoolExecutor);
//开启第二个异步任务
CompletableFuture<Void> cartInfoFuture = CompletableFuture.runAsync(() -> {
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
//2、远程查询购物车所有选中的购物项
List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
confirmVo.setItems(currentCartItems);
//feign在远程调用之前要构造请求,调用很多的拦截器
}, threadPoolExecutor).thenRunAsync(() -> {
List<OrderItemVo> items = confirmVo.getItems();
//获取全部商品的id
List<Long> skuIds = items.stream()
.map((itemVo -> itemVo.getSkuId()))
.collect(Collectors.toList());
//远程查询商品库存信息
R skuHasStock = wmsFeignService.getSkuHasStock(skuIds);
List<SkuStockVo> skuStockVos = skuHasStock.getData("data", new TypeReference<List<SkuStockVo>>() {});
if (skuStockVos != null && skuStockVos.size() > 0) {
//将skuStockVos集合转换为map
Map<Long, Boolean> skuHasStockMap = skuStockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
confirmVo.setStocks(skuHasStockMap);
}
},threadPoolExecutor);
//3、查询用户积分
Integer integration = memberResponseVo.getIntegration();
confirmVo.setIntegration(integration);
//4、价格数据自动计算
//TODO 5、防重令牌(防止表单重复提交)
//为用户设置一个token,三十分钟过期时间(存在redis)
String token = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(USER_ORDER_TOKEN_PREFIX+memberResponseVo.getId(),token,30, TimeUnit.MINUTES);
confirmVo.setOrderToken(token);
CompletableFuture.allOf(addressFuture,cartInfoFuture).get();
return confirmVo;
}
}
1.3、【会员模块】获取会员所有收货地址
1.3.1、controller
package com.atguigu.gulimall.member.controller;
@RestController
@RequestMapping("member/memberreceiveaddress")
public class MemberReceiveAddressController {
@Autowired
private MemberReceiveAddressService memberReceiveAddressService;
@GetMapping("/{memberId}/address")
public List<MemberReceiveAddressEntity> getAddress(@PathVariable("memberId") Long memberId) {
return memberReceiveAddressService.getAddress(memberId);
}
1.3.2、service
com.atguigu.gulimall.member.service.impl
@Override
public List<MemberReceiveAddressEntity> getAddress(Long memberId) {
return this.list(new QueryWrapper<MemberReceiveAddressEntity>().eq("member_id", memberId));
}
1.4、订单服务远程调用用户服务
package com.atguigu.gulimall.order.feign;
@FeignClient("gulimall-member")
public interface MemberFeignService {
/**
* 返回会员所有的收货地址列表
* @param memberId 会员ID
* @return
*/
@GetMapping("/member/memberreceiveaddress/{memberId}/address")
List<MemberAddressVo> getAddress(@PathVariable("memberId") Long memberId);
}
1.5、【购物车模块】 获取用户选择的所有CartItem
1.5.1、业务流程
- 首先通过用户ID在Redis中查询到购物车中的所有的购物项
- 通过 filter 过滤 用户购物车中被选择的购物项
- 查询数据库中当前购物项的价格,不能使用之前加入购物车的价格
- 编写远程 gulimall-product 服务中的 查询sku价格接口
1.5.2、编写Controller层接口
编写 gulimall-cart 服务中 package com.atguigu.cart.controller;
路径下的 CartController 类:
package com.atguigu.cart.controller;
@Controller
public class CartController {
@Autowired
CartService cartService;
@GetMapping("/currentUserCartItems")
@ResponseBody
public List<CartItem> getCurrentUserCartItems(){
return cartService.getUserCartItems();
}
//....
}
1.5.3、Service层实现类
编写 gulimall-cart 服务中 com.atguigu.cart.service.impl
路径中 CartServiceImpl 类
@Autowired
ProductFeignService productFeignService;
/**
* 获取用户选择的所有购物项
* @return
*/
@Override
public List<CartItem> getUserCartItems() {
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
if (userInfoTo.getUserId() == null) {
return null;
} else {
String cartKey = CART_PREFIX + userInfoTo.getUserId();
// 获取所有用户选择的购物项
List<CartItem> collect = getCartItems(cartKey).stream()
.filter(item -> item.getCheck())
.map(item->{
// TODO 1、更新为最新价格
R price = productFeignService.getPrice(item.getSkuId());
String data = (String) price.get("data");
item.setPrice(new BigDecimal(data));
return item;
})
.collect(Collectors.toList());
return collect;
}
}
1.5.4、【商品模块】获取指定商品的价格
Gulimall-product 服务中 com.atguigu.gulimall.product.app
路径下的 SkuInfoController
package com.atguigu.gulimall.product.app;
@RestController
@RequestMapping("product/skuinfo")
public class SkuInfoController {
@Autowired
private SkuInfoService skuInfoService;
/**
* 获取指定商品的价格
* @param skuId
* @return
*/
@GetMapping("/{skuId}/price")
public R getPrice(@PathVariable("skuId") Long skuId){
SkuInfoEntity skuInfoEntity = skuInfoService.getById(skuId);
return R.ok().setData(skuInfoEntity.getPrice().toString());
}
1.5.5、购物车服务远程调用商品服务
package com.atguigu.cart.feign;
@FeignClient("gulimall-product")
public interface ProductFeignService {
//.....
@GetMapping("/product/skuinfo/{skuId}/price")
R getPrice(@PathVariable("skuId") Long skuId);
}
1.5.6、订单服务远程调用购物车服务
package com.atguigu.gulimall.order.feign;
@FeignClient("gulimall-cart")
public interface CartFeignService {
@GetMapping("/currentUserCartItems")
List<OrderItemVo> getCurrentUserCartItems();
}
1.6、Feign远程调用丢失请求头问题
1.6.1、问题分析
问题 :Feign远程调用的时候会丢失请求头
原因:远程调用是一个新的请求,不携带之前请求的cookie,导致购物车服务得不到请求头cookie里的登录信息。
解决:加上feign远程调用的请求拦截器。(RequestInterceptor)
因为feign在远程调用之前会执行所有的RequestInterceptor拦截器
1.6.2、【订单模块】解决:配置类添加请求拦截器
新请求同步cookie到请求头里
package com.atguigu.gulimall.order.config;
@Configuration
public class GulimallFeignConfig {
/**
* feign在远程调用之前会执行所有的RequestInterceptor拦截器
* @return
*/
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor(){
@Override
public void apply(RequestTemplate requestTemplate) {
// 1、使用 RequestContextHolder 拿到请求数据,RequestContextHolder底层使用过线程共享数据 ThreadLocal<RequestAttributes>
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes!=null){
HttpServletRequest request = attributes.getRequest();
// 2、同步请求头数据,Cookie
String cookie = request.getHeader("Cookie");
// 给新请求同步了老请求的cookie
requestTemplate.header("Cookie",cookie);
}
}
};
}
}
1.7、异步线程丢失主线程请求头问题
问题演示,删除红框代码:
上面完整代码里service里,已经解决了异步编排请求头丢失问题,我们可以删除再调试:
发现报错,报错原因是没有登录(因为远程调用线程丢失了请求头,ThreadLocal里也就获取不到登录信息)。
问题:
由于 RequestContextHolder底层使用的是线程共享数据 ThreadLocal<RequestAttributes>
,我们知道线程共享数据的域是 当前线程下,线程之间是不共享的。所以在开启异步后,异步线程获取不到主线程请求的信息,自然也就无法共享cookie
了。
解决:
向异步 RequestContextHolder 线程域中放主线程的域。
修改 gulimall-order 服务中 com.atguigu.gulimall.order.service.impl
目录下的 OrderServiceImpl 类
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
// 获取主线程的域
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 1、远程查询所有的地址列表
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
// 将主线程的域放在该线程的域中
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
confirmVo.setAddressVos(address);
}, executor);
// 2、远程查询购物车所有选中的购物项
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
// 将老请求的域放在该线程的域中
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
}, executor);
// feign在远程调用请求之前要构造
// 3、查询用户积分
Integer integration = memberRespVo.getIntegration();
confirmVo.setIntegration(integration);
// 4、其他数据自动计算
// TODO 5、防重令牌
CompletableFuture.allOf(getAddressFuture,cartFuture).get();
return confirmVo;
}
1.8、前端,订单确认页渲染
修改 gulimall-order 服务中,src/main/resources/templates/
路径下的 confirm.html
<!--主体部分-->
<p class="p1">填写并核对订单信息</p>
<div class="section">
<!--收货人信息-->
<div class="top-2">
<span>收货人信息</span>
<span>新增收货地址</span>
</div>
<!--地址-->
<div class="top-3" th:each="addr:${orderConfirmData.addressVos}">
<p>[[${addr.name}]]</p><span>[[${addr.name}]] [[${addr.province}]] [[${addr.city}]] [[${addr.detailAddress}]] [[${addr.phone}]]</span>
</div>
<p class="p2">更多地址︾</p>
<div class="hh1"/></div>
<div class="xia">
<div class="qian">
<p class="qian_y">
<span>[[${orderConfirmData.count}]]</span>
<span>件商品,总商品金额:</span>
<span class="rmb">¥[[${#numbers.formatDecimal(orderConfirmData.total,1,2)}]]</span>
</p>
<p class="qian_y">
<span>返现:</span>
<span class="rmb"> -¥0.00</span>
</p>
<p class="qian_y">
<span>运费: </span>
<span class="rmb"> ¥0.00</span>
</p>
<p class="qian_y">
<span>服务费: </span>
<span class="rmb"> ¥0.00</span>
</p>
<p class="qian_y">
<span>退换无忧: </span>
<span class="rmb"> ¥0.00</span>
</p>
</div>
<div class="yfze">
<p class="yfze_a"><span class="z">应付总额:</span><span class="hq">¥[[${#numbers.formatDecimal(orderConfirmData.pryPrice,1,2)}]]</span></p>
<p class="yfze_b">寄送至: IT-中心研发二部 收货人:</p>
</div>
<button class="tijiao">提交订单</button>
</div>
1.9、订单确认页里,商品的库存查询
需求:
在远程查询购物车所有选中的购物项之后进行 批量查询库存
1)、在订单确认页数据获取 Service层实现类 OrderServiceImpl 方法中进行批量查询库存
Gulimall-order 服务中 com.atguigu.gulimall.order.service.impl
路径下的 OrderServiceImpl 类
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
// 获取主线程的请求域
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 1、远程查询所有的地址列表
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
// 将主线程的请求域放在该线程请求域中
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
confirmVo.setAddressVos(address);
}, executor);
// 2、远程查询购物车所有选中的购物项
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
// 将主线程的请求域放在该线程请求域中
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
}, executor).thenRunAsync(()->{
// 批量查询商品项库存
List<OrderItemVo> items = confirmVo.getItems();
List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
R hasStock = wareFeignService.getSkusHasStock(collect);
List<SkuStockVo> data = hasStock.getData(new TypeReference<List<SkuStockVo>>() {
});
if (data != null) {
Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
confirmVo.setStocks(map);
}
}, executor);
// feign在远程调用请求之前要构造
// 3、查询用户积分
Integer integration = memberRespVo.getIntegration();
confirmVo.setIntegration(integration);
// 4、其他数据自动计算
// TODO 5、防重令牌
CompletableFuture.allOf(getAddressFuture,cartFuture).get();
return confirmVo;
}
2)、在gulimall-order 服务中创建商品是否有库存的VO类
在 Gulimall-order 服务中 package com.atguigu.gulimall.order.vo
路径下创建 SkuStockVo 类
package com.atguigu.gulimall.order.vo;
@Data
public class SkuStockVo {
private Long skuId;
private Boolean hasStock;
}
3)、gulimall-ware 库存服务中提供 查询库存的接口
gulimall-ware 服务中 com.atguigu.gulimall.ware.controller
路径下的 WareSkuController 类,之前编写过。
package com.atguigu.gulimall.ware.controller;
@RestController
@RequestMapping("ware/waresku")
public class WareSkuController {
@Autowired
private WareSkuService wareSkuService;
// 查询sku是否有库存
@PostMapping("/hasstock")
public R getSkusHasStock(@RequestBody List<Long> skuIds){
// sku_id,stock
List<SkuHasStockVo> vos = wareSkuService.getSkusHasStock(skuIds);
return R.ok().setData(vos);
}
//....
}
gulimall-order 服务中编写远程调用 gulimall-ware 库存服务中 查询库存 feign接口
gulimall-order 服务下 com.atguigu.gulimall.order.feign
路径下:WareFeignService
package com.atguigu.gulimall.order.feign;
@FeignClient("gulimall-ware")
public interface WareFeignService {
@PostMapping("/ware/waresku/hasstock")
R getSkusHasStock(@RequestBody List<Long> skuIds);
}
4)、页面效果
[[${orderConfirmData.stocks[item.skuId]?"有货":"无货"}]]
<div class="mi">
<p>[[${item.title}]]<span style="color: red;"> ¥ [[${#numbers.formatDecimal(item.price,1,2)}]]</span> <span> x[[${item.count}]]</span> <span>[[${orderConfirmData.stocks[item.skuId]?"有货":"无货"}]]</span></p>
<p><span>0.095kg</span></p>
<p class="tui-1"><img src="/static/order/confirm/img/i_07.png" />支持7天无理由退货</p>
</div>
1.10、根据用户地址ID,返回详细地址并计算物流费
1.10.1、需求
需求:选择收货地址,计算物流费
1.10.2、前端,选择收货地址页面效果
function highlight(){
$(".addr-item p").css({"border": "2px solid gray"});
$(".addr-item p[def='1']").css({"border": "2px solid red"});
}
$(".addr-item p").click(function () {
$(".addr-item p").attr("def","0");
$(this).attr("def","1");
highlight();
// 获取当前地址id
var addrId = $(this).attr("addrId");
// 发送ajax获取运费信息
getFare(addrId);
});
function getFare(addrId) {
$.get("http://gulimall.cn/api/ware/wareinfo/fare?addrId="+addrId,function (resp) {
console.log(resp);
$("#fareEle").text(resp.data.fare);
var total = [[${orderConfirmData.total}]]
// 设置运费信息
$("#payPriceEle").text(total*1 + resp.data.fare*1);
// 设置收货人信息
$("#reciveAddressEle").text(resp.data.address.province+" " + resp.data.address.region+ "" + resp.data.address.detailAddress);
$("#reveiverEle").text(resp.data.address.name);
})
}
1.10.3、 模型类抽取
gulimall-ware 服务中 com.atguigu.gulimall.ware.vo
路径下的 Vo
@Data
public class FareVo {
private MemberAddressVo addressVo;
private BigDecimal fare;
}
1.10.4、controller
gulimall-ware仓储服务编写 根据用户地址,返回详细地址并计算物流费h
package com.atguigu.gulimall.ware.controller;
@RestController
@RequestMapping("ware/wareinfo")
public class WareInfoController {
@Autowired
private WareInfoService wareInfoService;
@GetMapping("/fare")
public R getFare(@RequestParam("addrId") Long addrId){
FareVo fare = wareInfoService.getFare(addrId);
return R.ok().setData(fare);
}
//...
}
1.10.5、仓库模块远程调用用户模块,查地址信息
package com.atguigu.gulimall.ware.feign;
@FeignClient("gulimall-member")
public interface MemberFeignService {
/**
* 根据地址id查询地址的详细信息
* @param id
* @return
*/
@RequestMapping("/member/memberreceiveaddress/info/{id}")
R addrInfo(@PathVariable("id") Long id);
}
1.10.6、service,根据地址id获取地址信息和费用
gulimall-ware 服务中 com.atguigu.gulimall.ware.service.impl
路径下 WareInfoServiceImpl 类
@Override
public FareVo getFare(Long addrId) {
FareVo fareVo = new FareVo();
R r = memberFeignService.addrInfo(addrId);
MemberAddressVo data = r.getData("memberReceiveAddress",new TypeReference<MemberAddressVo>() {
});
if (data!=null) {
// 简单处理:截取手机号最后一位作为邮费
String phone = data.getPhone();
String substring = phone.substring(phone.length() - 1, phone.length());
BigDecimal bigDecimal = new BigDecimal(substring);
fareVo.setAddressVo(data);
fareVo.setFare(bigDecimal);
return fareVo;
}
return null;
}
1.11、保证接口幂等性,防重复提交表单
1.11.1、幂等性概述
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的。
-
接口幂等性:
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用,比如说支付场景,用户购买了商品支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也交成了两条这就没有保证接口的幂等性。 - 哪些情况需要防止:
- 用户多次点击按钮
- 用户页面回退再次提交
- 微服务互相调用,由于网络问题,导致请求失败。feign 触发重试机制
其他业务情況
- 幂等性解决方案
- 1、token机制(令牌机制)本项目采用令牌机制
- 2、各种锁机制
- 3、各种唯一性约束
- 4、防重表
- 5、全球请求唯一id
1.11.2、任务幂等性的三种保证方法
-
数据库约束:比如唯一约束,主键。同一个主键不可能两次都插入成功。不推荐因为适用范围太窄,只适用于保存数据库前就已经设置好主键并且每次主键一样的情况下。
-
乐观锁:数据库表中增加一个版本字段,更新时判断是否等于某个版本。例如重复提交时判断数据库发现版本已被改变就不提交了。不推荐,因为要查数据库,给数据库压力,临时的操作我们尽量在缓存库里操作,降低数据库压力。
-
Redis唯一序列号(推荐):Redis键为任务id,值为随机序列化uuid。请求前生成唯一的序列号,携带序列号去请求,请求时在redis记录该序列号表示以该序列号的请求执行过了,如果相同的序列号再次来执行说明是重复执行。也可以通过让用户每次提交时输入验证码,提交后校验前后端验证码实现幂等性。
1.11.3、业务流程
需求:用户进入订单确认页,在不刷新、不重进的情况下,重复点击“提交订单”,只有一次能提交成功。
确认订单: 生成令牌:redis添加数据,key为"order:token"+用户id,value为防重复提交表单的uuid作为token,并设置30min过期时间。
提交订单(下一篇文章详细讲):
验令牌:先获取前端传来的token,再根据用户id查询Redis里的token,比较两个token是否相等,相等则代表是同一个的订单。因为uuid能保证唯一性,它是根据时间戳和mac地址生成的。
原子性验删令牌:验令牌和删除令牌写成一个lua脚本,Redis传参键值对并执行lua脚本,执行成功代表验证成功,执行失败代表验证失败。
1.11.4、代码实现,防重复提交表单,唯一序列号方式保证幂等性
gulimall-order服务 com.atguigu.gulimall.order.service.impl
路径下的 OrderServiceImpl
简洁版:
/**
* 确认订单、订单确认页返回需要用的数据
* @return
*/
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
//构建OrderConfirmVo,查登录、查询库存、购物车、商品id
//5、防重令牌(防止表单重复提交)
//为用户设置一个token,三十分钟过期时间(存在redis)
String token = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(USER_ORDER_TOKEN_PREFIX+memberResponseVo.getId(),token,30, TimeUnit.MINUTES);
confirmVo.setOrderToken(token);
return confirmVo;
}
/**
* 下单操作:验令牌、创建订单、验价格、验库存
* @param vo
* @return
*/
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
// 从拦截器中拿到当前的用户
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
// 1、验证令牌【令牌的对比和删除必须保证原子性】,通过使用脚本来完成(0:令牌校验失败; 1: 删除成功)
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
String orderToken = vo.getOrderToken();
// 原子验证令牌和删除令牌
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 {
// 令牌验证成功
return response;
}
}
前端会显示令牌:
<form action="http://order.gulimall.com/submitOrder" method="post">
<input id="addrInput" type="hidden" name="addrId" />
<input id="payPriceInput" type="hidden" name="payPrice">
<input name="orderToken" th:value="${confirmOrderData.orderToken}" type="hidden"/>
<button class="tijiao" type="submit">提交订单</button>
</form>
11.1.5 测试
重启服务,进入订单确认页,暂时删去前端<input>里的type="hidden",可以看见令牌:
测试发现,刷新页面,令牌会更改。回退后重新进入确认页,令牌会更改。
我(id是66)在不刷新的情况下,连续点击2次提交,传到后端的token都是一样的,记为token1,Redis存"order:token66"--->token1。文章来源:https://www.toymoban.com/news/detail-405826.html
- 线程A:“提交订单”的controller接收到token1,原子性对比redis里的token1和删除,校验通过;
- 线程B:“提交订单”的controller接收到token1,原子性对比redis里的token1和删除,因为线程A已经删除成功,所以现在校验失败或者删除失败,所以校验失败。
我刷新一下,再次点击2次提交,传到后端的token都是一样的,记为token2,Redis存"order:token66"--->token2。文章来源地址https://www.toymoban.com/news/detail-405826.html
- 线程C:“提交订单”的controller接收到token2,原子性对比redis里的token2和删除,校验通过;
- 线程D:“提交订单”的controller接收到token2,原子性对比redis里的token2和删除,因为线程C已经删除成功,所以现在校验失败或者删除失败,所以校验失败。
到了这里,关于谷粒商城笔记+踩坑(20)——订单确认页。feign、异步请求头丢失问题+令牌保证幂等性的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!