導(dǎo)航:
谷粒商城筆記+踩坑匯總篇
目錄
1、訂單確認(rèn)頁
1.1、vo類抽取
1.2、獲取訂單詳情頁數(shù)據(jù),完整代碼
1.2.1、Controller編寫跳轉(zhuǎn)訂單確認(rèn)頁方法
1.2.2、Service獲取訂單詳情頁數(shù)據(jù)
1.3、【會(huì)員模塊】獲取會(huì)員所有收貨地址
1.3.1、controller
1.3.2、service?
1.4、訂單服務(wù)遠(yuǎn)程調(diào)用用戶服務(wù)
1.5、【購物車模塊】 獲取用戶選擇的所有CartItem
1.5.1、業(yè)務(wù)流程?
1.5.2、編寫Controller層接口
1.5.3、Service層實(shí)現(xiàn)類
1.5.4、【商品模塊】獲取指定商品的價(jià)格
1.5.5、購物車服務(wù)遠(yuǎn)程調(diào)用商品服務(wù)?
1.5.6、訂單服務(wù)遠(yuǎn)程調(diào)用購物車服務(wù)
1.6、Feign遠(yuǎn)程調(diào)用丟失請求頭問題
1.6.1、問題分析?
1.6.2、【訂單模塊】解決:配置類添加請求攔截器
1.7、異步線程丟失主線程請求頭問題
1.8、前端,訂單確認(rèn)頁渲染
1.9、訂單確認(rèn)頁里,商品的庫存查詢
1.10、根據(jù)用戶地址ID,返回詳細(xì)地址并計(jì)算物流費(fèi)
1.10.1、需求?
1.10.2、前端,選擇收貨地址頁面效果
1.10.3、?模型類抽取?
1.10.4、controller
1.10.5、倉庫模塊遠(yuǎn)程調(diào)用用戶模塊,查地址信息
1.10.6、service,根據(jù)地址id獲取地址信息和費(fèi)用
1.11、保證接口冪等性,防重復(fù)提交表單
1.11.1、冪等性概述
1.11.2、任務(wù)冪等性的三種保證方法
1.11.3、業(yè)務(wù)流程
1.11.4、代碼實(shí)現(xiàn),防重復(fù)提交表單,唯一序列號方式保證冪等性
11.1.5 測試
1、訂單確認(rèn)頁
1.1、vo類抽取
訂單確認(rèn)頁需要用的數(shù)據(jù)
- 因?yàn)榇嬖诰W(wǎng)路延遲等問題,若一直點(diǎn)下單會(huì)下許多。所以我們需要防重令牌
com.atguigu.gulimall.order.vo
/**
* Description: 訂單確認(rèn)頁需要用的數(shù)據(jù)
*/
public class OrderConfirmVo {
/**
* 收貨地址,ums_member_receive_address 表
*/
@Setter@Getter
List<MemberAddressVo> addressVos;
/**
* 所有選中的購物車項(xiàng)
*/
@Setter@Getter
List<OrderItemVo> items;
// 發(fā)票記錄。。。
/**
* 優(yōu)惠券信息
*/
@Setter@Getter
Integer integration;
/**
* 是否有庫存
*/
@Setter@Getter
Map<Long,Boolean> stocks;
/**
* 防重令牌
*/
@Setter@Getter
String OrderToken;
/**
* @return 訂單總額
* 所有選中商品項(xiàng)的價(jià)格 * 其數(shù)量
*/
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;
}
/**
* 應(yīng)付價(jià)格
*/
//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;
/**
* 所有選中的購物車項(xiàng)
*/
List<OrderItemVo> items;
// 發(fā)票記錄。。。
/**
* 優(yōu)惠券信息
*/
Integer integration;
/**
* 訂單總額
*/
BigDecimal total;
/**
* 應(yīng)付價(jià)格
*/
BigDecimal pryPrice;
}
商品項(xiàng)信息
package com.atguigu.gulimall.order.vo;
@Data
public class OrderItemVo {
/**
* 商品Id
*/
private Long skuId;
/**
* 商品標(biāo)題
*/
private String title;
/**
* 商品圖片
*/
private String image;
/**
* 商品套餐信
*/
private List<String> skuAttr;
/**
* 商品價(jià)格
*/
private BigDecimal price;
/**
* 數(shù)量
*/
private Integer count;
/**
* 小計(jì)價(jià)格
*/
private BigDecimal totalPrice;
}
1.2、獲取訂單詳情頁數(shù)據(jù),完整代碼
1.2.1、Controller編寫跳轉(zhuǎn)訂單確認(rèn)頁方法
com.atguigu.gulimall.order.web
@Controller
public class OrderWebController {
@Autowired
OrderService orderService;
//去結(jié)算確認(rèn)頁
@GetMapping("/toTrade")
public String toTrade(Model model){
OrderConfirmVo confirmVo = orderService.confirmOrder();
model.addAttribute("OrderConfirmData",confirmVo);
return "confirm";
}
}
1.2.2、Service獲取訂單詳情頁數(shù)據(jù)
業(yè)務(wù)流程:
- 1、遠(yuǎn)程查詢所有的地址列表
- 2、遠(yuǎn)程查詢購物車所有選中的購物項(xiàng)
- 3、查詢用戶積分
- 4、其他數(shù)據(jù)自動(dòng)計(jì)算
- 5、防重令牌
?com.atguigu.gulimall.order.service.impl
?
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {
@Autowired
MemberFeignService memberFeignService;
@Autowired
CartFeignService cartFeignService;
/**
* 訂單確認(rèn)頁返回需要用的數(shù)據(jù)
* @return
*/
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
//構(gòu)建響應(yīng)模型類OrderConfirmVo
OrderConfirmVo confirmVo = new OrderConfirmVo();
//從攔截器ThreadLocal獲取當(dāng)前用戶登錄的信息
MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
//TODO :獲取當(dāng)前線程請求頭信息(解決Feign異步調(diào)用丟失請求頭問題)
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//開啟第一個(gè)異步任務(wù)
CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {
//每一個(gè)線程都來共享之前的請求數(shù)據(jù)
RequestContextHolder.setRequestAttributes(requestAttributes);
//1、遠(yuǎn)程查詢所有的收獲地址列表
List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId());
confirmVo.setMemberAddressVos(address);
}, threadPoolExecutor);
//開啟第二個(gè)異步任務(wù)
CompletableFuture<Void> cartInfoFuture = CompletableFuture.runAsync(() -> {
//每一個(gè)線程都來共享之前的請求數(shù)據(jù)
RequestContextHolder.setRequestAttributes(requestAttributes);
//2、遠(yuǎn)程查詢購物車所有選中的購物項(xiàng)
List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
confirmVo.setItems(currentCartItems);
//feign在遠(yuǎn)程調(diào)用之前要構(gòu)造請求,調(diào)用很多的攔截器
}, threadPoolExecutor).thenRunAsync(() -> {
List<OrderItemVo> items = confirmVo.getItems();
//獲取全部商品的id
List<Long> skuIds = items.stream()
.map((itemVo -> itemVo.getSkuId()))
.collect(Collectors.toList());
//遠(yuǎn)程查詢商品庫存信息
R skuHasStock = wmsFeignService.getSkuHasStock(skuIds);
List<SkuStockVo> skuStockVos = skuHasStock.getData("data", new TypeReference<List<SkuStockVo>>() {});
if (skuStockVos != null && skuStockVos.size() > 0) {
//將skuStockVos集合轉(zhuǎn)換為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、價(jià)格數(shù)據(jù)自動(dòng)計(jì)算
//TODO 5、防重令牌(防止表單重復(fù)提交)
//為用戶設(shè)置一個(gè)token,三十分鐘過期時(shí)間(存在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、【會(huì)員模塊】獲取會(huì)員所有收貨地址
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、訂單服務(wù)遠(yuǎn)程調(diào)用用戶服務(wù)
package com.atguigu.gulimall.order.feign;
@FeignClient("gulimall-member")
public interface MemberFeignService {
/**
* 返回會(huì)員所有的收貨地址列表
* @param memberId 會(huì)員ID
* @return
*/
@GetMapping("/member/memberreceiveaddress/{memberId}/address")
List<MemberAddressVo> getAddress(@PathVariable("memberId") Long memberId);
}
1.5、【購物車模塊】 獲取用戶選擇的所有CartItem
1.5.1、業(yè)務(wù)流程?
- 首先通過用戶ID在Redis中查詢到購物車中的所有的購物項(xiàng)
- 通過 filter 過濾 用戶購物車中被選擇的購物項(xiàng)
- 查詢數(shù)據(jù)庫中當(dāng)前購物項(xiàng)的價(jià)格,不能使用之前加入購物車的價(jià)格
- 編寫遠(yuǎn)程 gulimall-product 服務(wù)中的 查詢sku價(jià)格接口
1.5.2、編寫Controller層接口
編寫 gulimall-cart 服務(wù)中 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層實(shí)現(xiàn)類
編寫 gulimall-cart 服務(wù)中 com.atguigu.cart.service.impl
路徑中 CartServiceImpl 類
@Autowired
ProductFeignService productFeignService;
/**
* 獲取用戶選擇的所有購物項(xiàng)
* @return
*/
@Override
public List<CartItem> getUserCartItems() {
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
if (userInfoTo.getUserId() == null) {
return null;
} else {
String cartKey = CART_PREFIX + userInfoTo.getUserId();
// 獲取所有用戶選擇的購物項(xiàng)
List<CartItem> collect = getCartItems(cartKey).stream()
.filter(item -> item.getCheck())
.map(item->{
// TODO 1、更新為最新價(jià)格
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、【商品模塊】獲取指定商品的價(jià)格
Gulimall-product 服務(wù)中 com.atguigu.gulimall.product.app
路徑下的 SkuInfoController
package com.atguigu.gulimall.product.app;
@RestController
@RequestMapping("product/skuinfo")
public class SkuInfoController {
@Autowired
private SkuInfoService skuInfoService;
/**
* 獲取指定商品的價(jià)格
* @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、購物車服務(wù)遠(yuǎn)程調(diào)用商品服務(wù)?
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、訂單服務(wù)遠(yuǎn)程調(diào)用購物車服務(wù)
package com.atguigu.gulimall.order.feign;
@FeignClient("gulimall-cart")
public interface CartFeignService {
@GetMapping("/currentUserCartItems")
List<OrderItemVo> getCurrentUserCartItems();
}
?
1.6、Feign遠(yuǎn)程調(diào)用丟失請求頭問題
1.6.1、問題分析?
問題 :Feign遠(yuǎn)程調(diào)用的時(shí)候會(huì)丟失請求頭
原因:遠(yuǎn)程調(diào)用是一個(gè)新的請求,不攜帶之前請求的cookie,導(dǎo)致購物車服務(wù)得不到請求頭cookie里的登錄信息。
解決:加上feign遠(yuǎn)程調(diào)用的請求攔截器。(RequestInterceptor)
因?yàn)閒eign在遠(yuǎn)程調(diào)用之前會(huì)執(zhí)行所有的RequestInterceptor攔截器
1.6.2、【訂單模塊】解決:配置類添加請求攔截器
新請求同步cookie到請求頭里
package com.atguigu.gulimall.order.config;
@Configuration
public class GulimallFeignConfig {
/**
* feign在遠(yuǎn)程調(diào)用之前會(huì)執(zhí)行所有的RequestInterceptor攔截器
* @return
*/
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor(){
@Override
public void apply(RequestTemplate requestTemplate) {
// 1、使用 RequestContextHolder 拿到請求數(shù)據(jù),RequestContextHolder底層使用過線程共享數(shù)據(jù) ThreadLocal<RequestAttributes>
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes!=null){
HttpServletRequest request = attributes.getRequest();
// 2、同步請求頭數(shù)據(jù),Cookie
String cookie = request.getHeader("Cookie");
// 給新請求同步了老請求的cookie
requestTemplate.header("Cookie",cookie);
}
}
};
}
}
1.7、異步線程丟失主線程請求頭問題
問題演示,刪除紅框代碼:
上面完整代碼里service里,已經(jīng)解決了異步編排請求頭丟失問題,我們可以刪除再調(diào)試:
發(fā)現(xiàn)報(bào)錯(cuò),報(bào)錯(cuò)原因是沒有登錄(因?yàn)?strong>遠(yuǎn)程調(diào)用線程丟失了請求頭,ThreadLocal里也就獲取不到登錄信息)。
問題:
由于 RequestContextHolder底層使用的是線程共享數(shù)據(jù) ThreadLocal<RequestAttributes>
,我們知道線程共享數(shù)據(jù)的域是 當(dāng)前線程下,線程之間是不共享的。所以在開啟異步后,異步線程獲取不到主線程請求的信息,自然也就無法共享cookie
了。
解決:
向異步 RequestContextHolder 線程域中放主線程的域。
修改 gulimall-order 服務(wù)中 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、遠(yuǎn)程查詢所有的地址列表
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
// 將主線程的域放在該線程的域中
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
confirmVo.setAddressVos(address);
}, executor);
// 2、遠(yuǎn)程查詢購物車所有選中的購物項(xiàng)
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
// 將老請求的域放在該線程的域中
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
}, executor);
// feign在遠(yuǎn)程調(diào)用請求之前要構(gòu)造
// 3、查詢用戶積分
Integer integration = memberRespVo.getIntegration();
confirmVo.setIntegration(integration);
// 4、其他數(shù)據(jù)自動(dòng)計(jì)算
// TODO 5、防重令牌
CompletableFuture.allOf(getAddressFuture,cartFuture).get();
return confirmVo;
}
1.8、前端,訂單確認(rèn)頁渲染
修改 gulimall-order 服務(wù)中,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>返現(xiàn):</span>
<span class="rmb"> -¥0.00</span>
</p>
<p class="qian_y">
<span>運(yùn)費(fèi): </span>
<span class="rmb"> ? ¥0.00</span>
</p>
<p class="qian_y">
<span>服務(wù)費(fèi): </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">應(yīng)付總額:</span><span class="hq">¥[[${#numbers.formatDecimal(orderConfirmData.pryPrice,1,2)}]]</span></p>
<p class="yfze_b">寄送至: IT-中心研發(fā)二部 收貨人:</p>
</div>
<button class="tijiao">提交訂單</button>
</div>
1.9、訂單確認(rèn)頁里,商品的庫存查詢
需求:
在遠(yuǎn)程查詢購物車所有選中的購物項(xiàng)之后進(jìn)行 批量查詢庫存
1)、在訂單確認(rèn)頁數(shù)據(jù)獲取 Service層實(shí)現(xiàn)類 OrderServiceImpl 方法中進(jìn)行批量查詢庫存
Gulimall-order 服務(wù)中 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、遠(yuǎn)程查詢所有的地址列表
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
// 將主線程的請求域放在該線程請求域中
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
confirmVo.setAddressVos(address);
}, executor);
// 2、遠(yuǎn)程查詢購物車所有選中的購物項(xiàng)
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
// 將主線程的請求域放在該線程請求域中
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
}, executor).thenRunAsync(()->{
// 批量查詢商品項(xiàng)庫存
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在遠(yuǎn)程調(diào)用請求之前要構(gòu)造
// 3、查詢用戶積分
Integer integration = memberRespVo.getIntegration();
confirmVo.setIntegration(integration);
// 4、其他數(shù)據(jù)自動(dòng)計(jì)算
// TODO 5、防重令牌
CompletableFuture.allOf(getAddressFuture,cartFuture).get();
return confirmVo;
}
2)、在gulimall-order 服務(wù)中創(chuàng)建商品是否有庫存的VO類
在 Gulimall-order 服務(wù)中 package com.atguigu.gulimall.order.vo
路徑下創(chuàng)建 SkuStockVo 類
package com.atguigu.gulimall.order.vo;
@Data
public class SkuStockVo {
private Long skuId;
private Boolean hasStock;
}
3)、gulimall-ware 庫存服務(wù)中提供 查詢庫存的接口
gulimall-ware 服務(wù)中 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 服務(wù)中編寫遠(yuǎn)程調(diào)用 gulimall-ware 庫存服務(wù)中 查詢庫存 feign接口
gulimall-order 服務(wù)下 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、根據(jù)用戶地址ID,返回詳細(xì)地址并計(jì)算物流費(fèi)
1.10.1、需求?
需求:選擇收貨地址,計(jì)算物流費(fèi)
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();
// 獲取當(dāng)前地址id
var addrId = $(this).attr("addrId");
// 發(fā)送ajax獲取運(yùn)費(fèi)信息
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}]]
// 設(shè)置運(yùn)費(fèi)信息
$("#payPriceEle").text(total*1 + resp.data.fare*1);
// 設(shè)置收貨人信息
$("#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 服務(wù)中 com.atguigu.gulimall.ware.vo
路徑下的 Vo
@Data
public class FareVo {
private MemberAddressVo addressVo;
private BigDecimal fare;
}
1.10.4、controller
gulimall-ware倉儲服務(wù)編寫 根據(jù)用戶地址,返回詳細(xì)地址并計(jì)算物流費(fèi)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、倉庫模塊遠(yuǎn)程調(diào)用用戶模塊,查地址信息
package com.atguigu.gulimall.ware.feign;
@FeignClient("gulimall-member")
public interface MemberFeignService {
/**
* 根據(jù)地址id查詢地址的詳細(xì)信息
* @param id
* @return
*/
@RequestMapping("/member/memberreceiveaddress/info/{id}")
R addrInfo(@PathVariable("id") Long id);
}
1.10.6、service,根據(jù)地址id獲取地址信息和費(fèi)用
gulimall-ware 服務(wù)中 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) {
// 簡單處理:截取手機(jī)號最后一位作為郵費(fèi)
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、保證接口冪等性,防重復(fù)提交表單
1.11.1、冪等性概述
接口冪等性就是用戶對于同一操作發(fā)起的一次請求或者多次請求的結(jié)果是一致的。
-
接口冪等性:
接口冪等性就是用戶對于同一操作發(fā)起的一次請求或者多次請求的結(jié)果是一致的,不會(huì)因?yàn)槎啻吸c(diǎn)擊而產(chǎn)生了副作用,比如說支付場景,用戶購買了商品支付扣款成功,但是返回結(jié)果的時(shí)候網(wǎng)絡(luò)異常,此時(shí)錢已經(jīng)扣了,用戶再次點(diǎn)擊按鈕,此時(shí)會(huì)進(jìn)行第二次扣款,返回結(jié)果成功,用戶查詢余額返發(fā)現(xiàn)多扣錢了,流水記錄也交成了兩條這就沒有保證接口的冪等性。 - 哪些情況需要防止:
- 用戶多次點(diǎn)擊按鈕
- 用戶頁面回退再次提交
- 微服務(wù)互相調(diào)用,由于網(wǎng)絡(luò)問題,導(dǎo)致請求失敗。feign 觸發(fā)重試機(jī)制
其他業(yè)務(wù)情況
- 冪等性解決方案
- 1、token機(jī)制(令牌機(jī)制)本項(xiàng)目采用令牌機(jī)制
- 2、各種鎖機(jī)制
- 3、各種唯一性約束
- 4、防重表
- 5、全球請求唯一id
1.11.2、任務(wù)冪等性的三種保證方法
-
數(shù)據(jù)庫約束:比如唯一約束,主鍵。同一個(gè)主鍵不可能兩次都插入成功。不推薦因?yàn)檫m用范圍太窄,只適用于保存數(shù)據(jù)庫前就已經(jīng)設(shè)置好主鍵并且每次主鍵一樣的情況下。
-
樂觀鎖:數(shù)據(jù)庫表中增加一個(gè)版本字段,更新時(shí)判斷是否等于某個(gè)版本。例如重復(fù)提交時(shí)判斷數(shù)據(jù)庫發(fā)現(xiàn)版本已被改變就不提交了。不推薦,因?yàn)橐閿?shù)據(jù)庫,給數(shù)據(jù)庫壓力,臨時(shí)的操作我們盡量在緩存庫里操作,降低數(shù)據(jù)庫壓力。
-
Redis唯一序列號(推薦):Redis鍵為任務(wù)id,值為隨機(jī)序列化uuid。請求前生成唯一的序列號,攜帶序列號去請求,請求時(shí)在redis記錄該序列號表示以該序列號的請求執(zhí)行過了,如果相同的序列號再次來執(zhí)行說明是重復(fù)執(zhí)行。也可以通過讓用戶每次提交時(shí)輸入驗(yàn)證碼,提交后校驗(yàn)前后端驗(yàn)證碼實(shí)現(xiàn)冪等性。
1.11.3、業(yè)務(wù)流程
需求:用戶進(jìn)入訂單確認(rèn)頁,在不刷新、不重進(jìn)的情況下,重復(fù)點(diǎn)擊“提交訂單”,只有一次能提交成功。
確認(rèn)訂單:?生成令牌:redis添加數(shù)據(jù),key為"order:token"+用戶id,value為防重復(fù)提交表單的uuid作為token,并設(shè)置30min過期時(shí)間。?
提交訂單(下一篇文章詳細(xì)講):
驗(yàn)令牌:先獲取前端傳來的token,再根據(jù)用戶id查詢Redis里的token,比較兩個(gè)token是否相等,相等則代表是同一個(gè)的訂單。因?yàn)閡uid能保證唯一性,它是根據(jù)時(shí)間戳和mac地址生成的。
原子性驗(yàn)刪令牌:驗(yàn)令牌和刪除令牌寫成一個(gè)lua腳本,Redis傳參鍵值對并執(zhí)行l(wèi)ua腳本,執(zhí)行成功代表驗(yàn)證成功,執(zhí)行失敗代表驗(yàn)證失敗。
1.11.4、代碼實(shí)現(xiàn),防重復(fù)提交表單,唯一序列號方式保證冪等性
gulimall-order服務(wù) com.atguigu.gulimall.order.service.impl
路徑下的 OrderServiceImpl
簡潔版:
/**
* 確認(rèn)訂單、訂單確認(rèn)頁返回需要用的數(shù)據(jù)
* @return
*/
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
//構(gòu)建OrderConfirmVo,查登錄、查詢庫存、購物車、商品id
//5、防重令牌(防止表單重復(fù)提交)
//為用戶設(shè)置一個(gè)token,三十分鐘過期時(shí)間(存在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;
}
/**
* 下單操作:驗(yàn)令牌、創(chuàng)建訂單、驗(yàn)價(jià)格、驗(yàn)庫存
* @param vo
* @return
*/
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
// 從攔截器中拿到當(dāng)前的用戶
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
// 1、驗(yàn)證令牌【令牌的對比和刪除必須保證原子性】,通過使用腳本來完成(0:令牌校驗(yàn)失敗; 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();
// 原子驗(yàn)證令牌和刪除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(),
OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId(), orderToken);
if (result == 0L) {
// 令牌驗(yàn)證失敗
response.setCode(1);
return response;
} else {
// 令牌驗(yàn)證成功
return response;
}
}
前端會(huì)顯示令牌:
<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 測試
重啟服務(wù),進(jìn)入訂單確認(rèn)頁,暫時(shí)刪去前端<input>里的type="hidden",可以看見令牌:
測試發(fā)現(xiàn),刷新頁面,令牌會(huì)更改?;赝撕笾匦逻M(jìn)入確認(rèn)頁,令牌會(huì)更改。
我(id是66)在不刷新的情況下,連續(xù)點(diǎn)擊2次提交,傳到后端的token都是一樣的,記為token1,Redis存"order:token66"--->token1。文章來源:http://www.zghlxwxcb.cn/news/detail-405826.html
- 線程A:“提交訂單”的controller接收到token1,原子性對比redis里的token1和刪除,校驗(yàn)通過;
- 線程B:“提交訂單”的controller接收到token1,原子性對比redis里的token1和刪除,因?yàn)榫€程A已經(jīng)刪除成功,所以現(xiàn)在校驗(yàn)失敗或者刪除失敗,所以校驗(yàn)失敗。
我刷新一下,再次點(diǎn)擊2次提交,傳到后端的token都是一樣的,記為token2,Redis存"order:token66"--->token2。文章來源地址http://www.zghlxwxcb.cn/news/detail-405826.html
- 線程C:“提交訂單”的controller接收到token2,原子性對比redis里的token2和刪除,校驗(yàn)通過;
- 線程D:“提交訂單”的controller接收到token2,原子性對比redis里的token2和刪除,因?yàn)榫€程C已經(jīng)刪除成功,所以現(xiàn)在校驗(yàn)失敗或者刪除失敗,所以校驗(yàn)失敗。
到了這里,關(guān)于谷粒商城筆記+踩坑(20)——訂單確認(rèn)頁。feign、異步請求頭丟失問題+令牌保證冪等性的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!