日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 综合教程 >内容正文

综合教程

谷粒商城--订单服务--高级篇笔记十一

發布時間:2023/12/4 综合教程 35 生活家
生活随笔 收集整理的這篇文章主要介紹了 谷粒商城--订单服务--高级篇笔记十一 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

1.頁面環境搭建

1.1 靜態資源導入nginx

等待付款 --------->detail

訂單頁 --------->list

結算頁 --------->confirm

收銀頁 ---------> pay

1.2 配置host

# gulimall
192.168.157.128 gulimall.com
# search
192.168.157.128 search.gulimall.com
# item 商品詳情
192.168.157.128 item.gulimall.com
#商城認證
192.168.157.128 auth.gulimall.com
#購物車
192.168.157.128 cart.gulimall.com
#訂單
192.168.157.128 order.gulimall.com
#單點登錄
127.0.0.1 ssoserver.com127.0.0.1 client1.com127.0.0.1 client2.com

1.3 配置網關

gulimall-gateway/src/main/resources/application.yml

        #訂單- id: gulimall_order_routeuri: lb://gulimall-orderpredicates:- Host=order.gulimall.com

1.4 開啟注冊發現

@EnableDiscoveryClient

1.5 新增依賴

        <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency>

1.6 修改各個頁面的靜態資源路徑

src=" ===>src="/static/order/xxx/

herf=" ===>herf="/static/order/xxx/

1.7 測試

1.7.1 訂單確認頁

確認頁前端代碼:https://gitee.com/zhourui815/gulimall/blob/master/gulimall-order/src/main/resources/templates/confirm.html

order.gulimall.com/confirm.html

1.7.2 訂單列表頁

訂單列表頁前端代碼:https://gitee.com/zhourui815/gulimall/blob/master/gulimall-order/src/main/resources/templates/list.html

谷粒商城訂單 (gulimall.com)

1.7.3 訂單詳情頁

訂單詳情頁前端代碼:https://gitee.com/zhourui815/gulimall/blob/master/gulimall-order/src/main/resources/templates/detail.html

order.gulimall.com/detail.html

1.7.4 訂單支付頁

訂單支付頁前端代碼:https://gitee.com/zhourui815/gulimall/blob/master/gulimall-order/src/main/resources/templates/pay.html

order.gulimall.com/pay.html

2. 整合Spring Session

2.1 導入依賴

        <dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId></dependency><!--redis--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><exclusions><exclusion><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId></exclusion></exclusions></dependency><!--jedis,redis客戶端--><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId></dependency>

2.2 開啟Spring Session

@EnableRedisHttpSession  //整合Redis作為session存儲

2.3 配置Spring Session存儲方式

  redis:host: 192.168.157.128session:store-type: redis

2.4 SpringSession 自定義

package site.zhourui.gulimall.order.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;/*** @author zr* @date 2021/12/12 10:29*/
@Configuration
public class GulimallSessionConfig {@Beanpublic CookieSerializer cookieSerializer() {DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();//放大作用域cookieSerializer.setDomainName("gulimall.com");cookieSerializer.setCookieName("GULISESSION");cookieSerializer.setCookieMaxAge(60*60*24*7);return cookieSerializer;}//session存儲對象方式json,默認jdk@Beanpublic RedisSerializer<Object> springSessionDefaultRedisSerializer() {return new GenericJackson2JsonRedisSerializer();}
}

2.5 整合后效果

可以實現登錄成功后用戶信息共享

3. 整合線程池

3.1 自定義線程池配置

gulimall-order/src/main/java/site/zhourui/gulimall/order/config/MyThreadConfig.java

package site.zhourui.gulimall.order.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;/*** @author zr* @date 2021/11/28 10:12*/@Configuration
public class MyThreadConfig {@Beanpublic ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool) {return new ThreadPoolExecutor(pool.getCoreSize(),pool.getMaxSize(),pool.getKeepAliveTime(),TimeUnit.SECONDS,new LinkedBlockingDeque<>(100000),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());}
}

gulimall-order/src/main/java/site/zhourui/gulimall/order/config/ThreadPoolConfigProperties.java

package site.zhourui.gulimall.order.config;import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;@ConfigurationProperties(prefix = "gulimall.thread")
@Component
@Data
public class ThreadPoolConfigProperties {private Integer coreSize;private Integer maxSize;private Integer keepAliveTime;
}

3.2 配置

gulimall:thread:core-size: 20max-size: 200keep-alive-time: 10

4. 訂單中心(理論)

電商系統涉及到 3 流, 分別時信息流, 資金流, 物流, 而訂單系統作為中樞將三者有機的集合起來。訂單模塊是電商系統的樞紐, 在訂單這個環節上需求獲取多個模塊的數據和信息, 同時對這些信息進行加工處理后流向下個環節, 這一系列就構成了訂單的信息流通。

4.1 訂單的構成

4.1.1 用戶信息

用戶信息包括用戶賬號、 用戶等級、 用戶的收貨地址、 收貨人、 收貨人電話等組成, 用戶賬戶需要綁定手機號碼, 但是用戶綁定的手機號碼不一定是收貨信息上的電話。 用戶可以添加多個收貨信息, 用戶等級信息可以用來和促銷系統進行匹配, 獲取商品折扣, 同時用戶等級還可以獲取積分的獎勵等

4.1.2 訂單基礎信息

訂單基礎信息是訂單流轉的核心, 其包括訂單類型、 父/子訂單、 訂單編號、 訂單狀態、 訂單流轉的時間等。

(1) 訂單類型包括實體商品訂單和虛擬訂單商品等, 這個根據商城商品和服務類型進行區分。
(2) 同時訂單都需要做父子訂單處理, 之前在初創公司一直只有一個訂單, 沒有做父子訂單處理后期需要進行拆單的時候就比較麻煩, 尤其是多商戶商場, 和不同倉庫商品的時候,父子訂單就是為后期做拆單準備的。
(3) 訂單編號不多說了, 需要強調的一點是父子訂單都需要有訂單編號, 需要完善的時候可以對訂單編號的每個字段進行統一定義和詮釋。
(4) 訂單狀態記錄訂單每次流轉過程, 后面會對訂單狀態進行單獨的說明。
(5) 訂單流轉時間需要記錄下單時間, 支付時間, 發貨時間, 結束時間/關閉時間等等

4.1.3 商品信息

商品信息從商品庫中獲取商品的 SKU 信息、 圖片、 名稱、 屬性規格、 商品單價、 商戶信息等, 從用戶下單行為記錄的用戶下單數量, 商品合計價格等。

4.1.4 優惠信息

優惠信息記錄用戶參與的優惠活動, 包括優惠促銷活動, 比如滿減、 滿贈、 秒殺等, 用戶使用的優惠券信息, 優惠券滿足條件的優惠券需要默認展示出來, 具體方式已在之前的優惠券篇章做過詳細介紹, 另外還虛擬幣抵扣信息等進行記錄。

4.1.4.1為什么把優惠信息單獨拿出來而不放在支付信息里面呢?

因為優惠信息只是記錄用戶使用的條目, 而支付信息需要加入數據進行計算, 所以做為區分。

4.1.5 支付信息

( 1) 支付流水單號, 這個流水單號是在喚起網關支付后支付通道返回給電商業務平臺的支付流水號, 財務通過訂單號和流水單號與支付通道進行對賬使用。
( 2) 支付方式用戶使用的支付方式, 比如微信支付、 支付寶支付、 錢包支付、 快捷支付等。支付方式有時候可能有兩個——余額支付+第三方支付。
( 3) 商品總金額, 每個商品加總后的金額; 運費, 物流產生的費用; 優惠總金額, 包括促銷活動的優惠金額, 優惠券優惠金額, 虛擬積分或者虛擬幣抵扣的金額, 會員折扣的金額等之和; 實付金額, 用戶實際需要付款的金額。用戶實付金額=商品總金額+運費-優惠總金額

4.1.6 物流信息

物流信息包括配送方式, 物流公司, 物流單號, 物流狀態, 物流狀態可以通過第三方接口來獲取和向用戶展示物流每個狀態節點。

4.2 訂單狀態

  1. 待付款
    用戶提交訂單后, 訂單進行預下單, 目前主流電商網站都會喚起支付, 便于用戶快速完成支付, 需要注意的是待付款狀態下可以對庫存進行鎖定, 鎖定庫存需要配置支付超時時間, 超時后將自動取消訂單, 訂單變更關閉狀態。
  2. 已付款/待發貨
    用戶完成訂單支付, 訂單系統需要記錄支付時間, 支付流水單號便于對賬, 訂單下放到 WMS系統, 倉庫進行調撥, 配貨, 分揀, 出庫等操作。
  3. 待收貨/已發貨
    倉儲將商品出庫后, 訂單進入物流環節, 訂單系統需要同步物流信息, 便于用戶實時知悉物品物流狀態
  4. 已完成
    用戶確認收貨后, 訂單交易完成。 后續支付側進行結算, 如果訂單存在問題進入售后狀態
  5. 已取消
    付款之前取消訂單。 包括超時未付款或用戶商戶取消訂單都會產生這種訂單狀態。
  6. 售后中
    用戶在付款后申請退款, 或商家發貨后用戶申請退換貨。售后也同樣存在各種狀態, 當發起售后申請后生成售后訂單, 售后訂單狀態為待審核, 等待商家審核, 商家審核通過后訂單狀態變更為待退貨, 等待用戶將商品寄回, 商家收貨后訂單狀態更新為待退款狀態, 退款到用戶原賬戶后訂單狀態更新為售后成功。

4.3 訂單流程

訂單流程是指從訂單產生到完成整個流轉的過程, 從而行程了一套標準流程規則。 而不同的產品類型或業務類型在系統中的流程會千差萬別, 比如上面提到的線上實物訂單和虛擬訂單的流程, 線上實物訂單與 O2O 訂單等, 所以需要根據不同的類型進行構建訂單流程。不管類型如何訂單都包括正向流程和逆向流程, 對應的場景就是購買商品和退換貨流程, 正向流程就是一個正常的網購步驟: 訂單生成–>支付訂單–>賣家發貨–>確認收貨–>交易成功。而每個步驟的背后, 訂單是如何在多系統之間交互流轉的, 可概括如下圖

4.3.1 訂單創建與支付 (重點)

  1. 訂單創建前需要預覽訂單, 選擇收貨信息等
  2. 訂單創建需要鎖定庫存, 庫存有才可創建, 否則不能創建
  3. 訂單創建后超時未支付需要解鎖庫存
  4. 支付成功后, 需要進行拆單, 根據商品打包方式, 所在倉庫, 物流等進行拆單
  5. 支付的每筆流水都需要記錄, 以待查賬
  6. 訂單創建, 支付成功等狀態都需要給 MQ 發送消息, 方便其他系統感知訂閱

4.3.2 逆向流程

  1. 修改訂單, 用戶沒有提交訂單, 可以對訂單一些信息進行修改, 比如配送信息,優惠信息, 及其他一些訂單可修改范圍的內容, 此時只需對數據進行變更即可。
  2. 訂單取消, 用戶主動取消訂單和用戶超時未支付, 兩種情況下訂單都會取消訂單, 而超時情況是系統自動關閉訂單, 所以在訂單支付的響應機制上面要做支付的限時處理, 尤其是在前面說的下單減庫存的情形下面, 可以保證快速的釋放庫存。另外需要需要處理的是促銷優惠中使用的優惠券, 權益等視平臺規則, 進行相應補回給用戶。
  3. 退款, 在待發貨訂單狀態下取消訂單時, 分為缺貨退款和用戶申請退款。 如果是全部退款則訂單更新為關閉狀態, 若只是做部分退款則訂單仍需進行進行, 同時生成一條退款的售后訂單, 走退款流程。 退款金額需原路返回用戶的賬戶。
  4. 發貨后的退款, 發生在倉儲貨物配送, 在配送過程中商品遺失, 用戶拒收, 用戶收貨后對商品不滿意, 這樣情況下用戶發起退款的售后訴求后, 需要商戶進行退款的審核, 雙方達成一致后, 系統更新退款狀態, 對訂單進行退款操作, 金額原路返回用戶的賬戶, 同時關閉原訂單數據。 僅退款情況下暫不考慮倉庫系統變化。 如果發生雙方協調不一致情況下, 可以申請平臺客服介入。 在退款訂單商戶不處理的情況下, 系統需要做限期判斷, 比如 5 天商戶不處理, 退款單自動變更同意退款。

5. 訂單中心(代碼)

5.1 訂單登錄攔截

因為訂單系統必然涉及到用戶信息,因此進入訂單系統的請求必須是已經登錄的,所以我們需要通過攔截器對未登錄訂單請求進行攔截

gulimall-order/src/main/java/site/zhourui/gulimall/order/interceptor/LoginUserInterceptor.java

package site.zhourui.gulimall.order.interceptor;/*** @author zr* @date 2021/12/21 22:04*/import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import site.zhourui.common.constant.AuthServerConstant;
import site.zhourui.common.vo.MemberResponseVo;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.PrintWriter;import static site.zhourui.common.constant.AuthServerConstant.LOGIN_USER;/*** 登錄攔截器* 從session中獲取了登錄信息(redis中),封裝到了ThreadLocal中*/
@Component
public class LoginUserInterceptor implements HandlerInterceptor {public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>();@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {HttpSession session = request.getSession();MemberResponseVo memberResponseVo = (MemberResponseVo) session.getAttribute(AuthServerConstant.LOGIN_USER);if (memberResponseVo != null) {loginUser.set(memberResponseVo);return true;}else {session.setAttribute("msg","請先登錄");response.sendRedirect("http://auth.gulimall.com/login.html");return false;}}
}

gulimall-order/src/main/java/site/zhourui/gulimall/order/config/OrderWebConfig.java

package site.zhourui.gulimall.order.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import site.zhourui.gulimall.order.interceptor.LoginUserInterceptor;/*** @author zr* @date 2021/12/21 22:05*/
@Configuration
public class OrderWebConfig implements WebMvcConfigurer {@Autowiredprivate LoginUserInterceptor loginUserInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");}
}

5.2 訂單確認頁

5.2.1 模型抽取

確認頁提交數據

gulimall-order/src/main/java/site/zhourui/gulimall/order/vo/OrderConfirmVo.java

package site.zhourui.gulimall.order.vo;import lombok.Getter;
import lombok.Setter;import java.math.BigDecimal;
import java.util.List;
import java.util.Map;/*** 訂單確認頁需要用的數據* @author zr* @date 2021/12/21 22:22*/
public class OrderConfirmVo {@Getter@SetterList<MemberAddressVo> memberAddressVos;/** 會員收獲地址列表 **/@Getter @SetterList<OrderItemVo> items;    /** 所有選中的購物項【購物車中的所有項】 **/@Getter @Setterprivate Integer integration;/** 優惠券(會員積分) **//** TODO 防止重復提交的令牌 冪等性**/@Getter @Setterprivate String orderToken;@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;if (items != null && items.size() > 0) {for (OrderItemVo item : items) {//計算當前商品的總價格BigDecimal itemPrice = item.getPrice().multiply(new BigDecimal(item.getCount().toString()));//再計算全部商品的總價格totalNum = totalNum.add(itemPrice);}}return totalNum;}/** 應付總額 **///BigDecimal payPrice;public BigDecimal getPayPrice() {return getTotal();}
}

確認頁提交數據模型還需要地址信息

gulimall-order/src/main/java/site/zhourui/gulimall/order/vo/MemberAddressVo.java

package site.zhourui.gulimall.order.vo;import lombok.Data;/*** 地址信息* @author zr* @date 2021/12/21 22:24*/
@Data
public class MemberAddressVo {private Long id;/*** member_id*/private Long memberId;/*** 收貨人姓名*/private String name;/*** 電話*/private String phone;/*** 郵政編碼*/private String postCode;/*** 省份/直轄市*/private String province;/*** 城市*/private String city;/*** 區*/private String region;/*** 詳細地址(街道)*/private String detailAddress;/*** 省市區代碼*/private String areacode;/*** 是否默認*/private Integer defaultStatus;}

確認頁提交數據模型還需要訂單行信息

gulimall-order/src/main/java/site/zhourui/gulimall/order/vo/OrderItemVo.java

package site.zhourui.gulimall.order.vo;import lombok.Data;import java.math.BigDecimal;
import java.util.List;/*** 購物項內容* @author zr* @date 2021/12/21 22:23*/
@Data
public class OrderItemVo {private Long skuId;             // skuIdprivate Boolean check = true;   // 是否選中private String title;           // 標題private String image;           // 圖片private List<String> skuAttrValues;// 商品銷售屬性private BigDecimal price;       // 單價private Integer count;          // 當前商品數量private BigDecimal totalPrice;  // 總價private BigDecimal weight = new BigDecimal("0.085");// 商品重量
}

5.2.2 提交確認訂單

5.2.2.1 訂單確認頁流程

1、遠程調用:獲取所有收貨地址【member-ums表】
2、遠程調用:所有選中的商品(最新價格-遠程調用)【cart-redis中】【product-查詢最新價格】
3、查詢用戶積分【session的用戶信息中】
4、訂單總額【根據所有選中的價格之和 求得】
5、應付總額【暫時跟訂單總額相等】【優惠卡等功能不做,直接用積分】

6、查詢每個商品是否有貨【批量查詢ware服務】
7、收貨地址高亮【選中地址調用ajax直接遠程調用ware計算運費【遠程調用會員服務member傳入addrId獲取詳細地址】
WareInfoController /fare
接口返回運費信息,和地址信息
8、防重令牌【防止用戶多次 提交訂單】【點擊提交訂單后,數據庫只保存一條訂單信息(冪等性,提交1次和多次結果是一致的)】

5.2.2.2 去到訂單確認頁面

返回訂單確認頁所需要的數據OrderConfirmVo

gulimall-order/src/main/java/site/zhourui/gulimall/order/web/OrderWebController.java

    /*** 去結算確認頁* @param model* @param request* @return* @throws ExecutionException* @throws InterruptedException*/@GetMapping(value = "/toTrade")public String toTrade(Model model, HttpServletRequest request) throws ExecutionException, InterruptedException {OrderConfirmVo confirmVo = orderService.confirmOrder();model.addAttribute("confirmOrderData",confirmVo);//展示訂單確認的數據return "confirm";}

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/OrderService.java

    OrderConfirmVo confirmOrder();

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

  1. 判斷用戶登錄信息
  2. 遠程查詢所有的收獲地址列表
  3. 遠程查詢購物車所有選中的購物項
  4. 遠程查詢商品庫存信息
  5. 查詢用戶積分
  6. 價格數據自動計算
  7. 防重令牌(防止表單重復提交)
/*** 訂單確認頁返回需要用的數據* @return*/@Overridepublic OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {//構建OrderConfirmVoOrderConfirmVo confirmVo = new OrderConfirmVo();//獲取當前用戶登錄的信息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(() -> {//每一個線程都來共享之前的請求數據【解決異步ThreadLocal 無法共享數據】RequestContextHolder.setRequestAttributes(requestAttributes);//2、遠程查詢購物車所有選中的購物項List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();confirmVo.setItems(currentCartItems);//feign在遠程調用之前要構造請求,調用很多的攔截器}, threadPoolExecutor).thenRunAsync(() -> {List<OrderItemVo> items = confirmVo.getItems();//獲取全部商品的idList<Long> skuIds = items.stream().map((itemVo -> itemVo.getSkuId())).collect(Collectors.toList());//3、遠程查詢商品庫存信息R skuHasStock = wmsFeignService.getSkuHasStock(skuIds);List<SkuStockVo> skuStockVos = skuHasStock.getData("data", new TypeReference<List<SkuStockVo>>() {});if (skuStockVos != null && skuStockVos.size() > 0) {//將skuStockVos集合轉換為mapMap<Long, Boolean> skuHasStockMap = skuStockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));confirmVo.setStocks(skuHasStockMap);}},threadPoolExecutor);//4、、查詢用戶積分Integer integration = memberResponseVo.getIntegration();confirmVo.setIntegration(integration);//5、、價格數據自動計算//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;}
5.2.2.2.1 遠程查詢所有的收獲地址列表

模擬創建兩條該用戶地址信息

gulimall-member/src/main/java/site/zhourui/gulimall/member/service/MemberReceiveAddressService.java

    List<MemberReceiveAddressEntity> getAddress(Long memberId);

gulimall-member/src/main/java/site/zhourui/gulimall/member/service/impl/MemberReceiveAddressServiceImpl.java

    @Overridepublic List<MemberReceiveAddressEntity> getAddress(Long memberId) {List<MemberReceiveAddressEntity> addressList = this.baseMapper.selectList(new QueryWrapper<MemberReceiveAddressEntity>().eq("member_id", memberId));return addressList;}

gulimall-member/src/main/java/site/zhourui/gulimall/member/controller/MemberReceiveAddressController.java

    /*** 根據會員id查詢會員的所有地址* @param memberId* @return*/@GetMapping(value = "/{memberId}/address")public List<MemberReceiveAddressEntity> getAddress(@PathVariable("memberId") Long memberId) {List<MemberReceiveAddressEntity> addressList = memberReceiveAddressService.getAddress(memberId);return addressList;}

gulimall-order/src/main/java/site/zhourui/gulimall/order/feign/MemberFeignService.java

package site.zhourui.gulimall.order.feign;import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import site.zhourui.gulimall.order.vo.MemberAddressVo;import java.util.List;/*** @author zr* @date 2021/12/23 15:05*/
@FeignClient("gulimall-member")
public interface MemberFeignService {/*** 查詢當前用戶的全部收貨地址* @param memberId* @return*/@GetMapping(value = "/member/memberreceiveaddress/{memberId}/address")List<MemberAddressVo> getAddress(@PathVariable("memberId") Long memberId);
}
5.2.2.2.2 遠程查詢購物車所有選中的購物項

gulimall-cart/src/main/java/site/zhourui/gulimall/cart/service/CartService.java

    /*** 獲取當前用戶的購物車所有商品項* @return*/List<CartItemVo> getUserCartItems();

gulimall-cart/src/main/java/site/zhourui/gulimall/cart/service/Impl/CartServiceImpl.java

    /*** 遠程調用:訂單服務調用【更新最新價格】* 獲取當前用戶購物車所有選中的商品項check=true【從redis中取】*/@Overridepublic List<CartItemVo> getUserCartItems() {List<CartItemVo> cartItemVoList = new ArrayList<>();//獲取當前用戶登錄的信息UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();//如果用戶未登錄直接返回nullif (userInfoTo.getUserId() == null) {return null;} else {//獲取購物車項String cartKey =CartConstant.CART_PREFIX + userInfoTo.getUserId();//獲取所有的List<CartItemVo> cartItems = getCartItems(cartKey);if (cartItems == null) {throw new CartExceptionHandler();}//篩選出選中的cartItemVoList = cartItems.stream().filter(items -> items.getCheck()).map(item -> {//更新為最新的價格(查詢數據庫)// redis中的價格不是最新的BigDecimal price = productFeignService.getPrice(item.getSkuId());item.setPrice(price);return item;}).collect(Collectors.toList());}return cartItemVoList;}

gulimall-cart/src/main/java/site/zhourui/gulimall/cart/controller/CartController.java

    /*** 訂單服務調用:【購物車頁面點擊確認訂單時】* 返回所有選中的商品項【從redis中取】* 并且要獲取最新的商品價格信息,而不是redis中的數據** 獲取當前用戶的購物車所有商品項*/@GetMapping(value = "/currentUserCartItems")@ResponseBodypublic List<CartItemVo> getCurrentCartItems() {List<CartItemVo> cartItemVoList = cartService.getUserCartItems();return cartItemVoList;}

gulimall-order/src/main/java/site/zhourui/gulimall/order/feign/CartFeignService.java

package site.zhourui.gulimall.order.feign;import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import site.zhourui.gulimall.order.vo.OrderItemVo;import java.util.List;/*** @author zr* @date 2021/12/23 15:06*/
@FeignClient("gulimall-cart")
public interface CartFeignService {/*** 查詢當前用戶購物車選中的商品項* @return*/@GetMapping(value = "/currentUserCartItems")List<OrderItemVo> getCurrentCartItems();}
5.2.2.2.3 遠程查詢商品庫存信息

gulimall-order/src/main/java/site/zhourui/gulimall/order/vo/SkuStockVo.java

返回庫存信息的vo

	package site.zhourui.gulimall.order.vo;import lombok.Data;/*** 庫存vo* @author zr* @date 2021/12/23 15:53*/
@Data
public class SkuStockVo {private Long skuId;private Boolean hasStock;}

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/WareSkuService.java

    /*** 判斷是否有庫存*/List<SkuHasStockVo> getSkusHasStock(List<Long> skuIds);

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/impl/WareSkuServiceImpl.java

    /***  檢查sku 是否有庫存*/@Overridepublic List<SkuHasStockVo> getSkusHasStock(List<Long> skuIds) {List<SkuHasStockVo> vos = skuIds.stream().map(skuId -> {SkuHasStockVo vo = new SkuHasStockVo();// 1、不止一個倉庫有,多個倉庫都有庫存 sum// 2、鎖定庫存是別人下單但是還沒下完Long count = baseMapper.getSkuStock(skuId);vo.setSkuId(skuId);vo.setHasStock(count == null ? false : count > 0);return vo;}).collect(Collectors.toList());return vos;}

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/controller/WareSkuController.java

    /*** 查詢sku是否有庫存*/@PostMapping("/hasstock")// @RequiresPermissions("ware:waresku:list")public R getSkusHasStock(@RequestBody List<Long> skuIds){// sku_id  stockList<SkuHasStockVo> vos = wareSkuService.getSkusHasStock(skuIds);return R.ok().setData(vos);}

gulimall-order/src/main/java/site/zhourui/gulimall/order/feign/WmsFeignService.java

package site.zhourui.gulimall.order.feign;import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import site.zhourui.common.utils.R;import java.util.List;/*** @author zr* @date 2021/12/23 15:06*/
@FeignClient("gulimall-ware")
public interface WmsFeignService {/*** 查詢sku是否有庫存*/@PostMapping(value = "/ware/waresku/hasstock")R getSkuHasStock(@RequestBody List<Long> skuIds);}
5.2.2.2.4 多線程異步編排

之前在章節3整合過線程池了,只需導入

    @Autowiredprivate ThreadPoolExecutor threadPoolExecutor;
5.2.2.3 feign遠程調用丟失請求頭

原因:feign發送請求時構造的RequestTemplate沒有請求頭(該請求頭為空),請求參數等信息【cookie沒了】

導致在cart服務中,攔截器攔截獲取session中的登錄信息,獲取不到userId【沒有cookie】

解決:同步新、老請求(老請求就是/toTrade請求,帶有Cookie數據)的cookie

原理: feign在遠程調用之前要構造請求,調用很多的攔截器(DEBUG,查看到會調用 攔截器)

gulimall-order/src/main/java/site/zhourui/gulimall/order/config/GuliFeignConfig.java

package site.zhourui.gulimall.order.config;/*** @author zr* @date 2021/12/23 17:03*/import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;/*** feign攔截器功能* 解決feign 遠程請求頭丟失問題**/
@Configuration
public class GuliFeignConfig {@Bean("requestInterceptor")public RequestInterceptor requestInterceptor() {RequestInterceptor requestInterceptor = new RequestInterceptor() {@Overridepublic void apply(RequestTemplate template) {System.out.println("feign遠程調用,攔截包裝請求頭");//1、使用RequestContextHolder拿到剛進來的請求數據ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (requestAttributes != null) {HttpServletRequest request = requestAttributes.getRequest();//老請求if (request != null) {//2、同步請求頭的數據(主要是cookie)//把老請求的cookie值放到新請求上來,進行一個同步String cookie = request.getHeader("Cookie");template.header("Cookie", cookie);}}}};return requestInterceptor;}}
5.2.2.4 Feign異步情況丟失上下文問題

導致攔截器中 空指針異常
1、先在主線程的ThreadLocal中獲取 請求頭數據

        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

2、再在新線程給ThreadLocal設置 請求頭數據【否則獲取不到數據,不是同一個線程】

          //每一個線程都來共享之前的請求數據【解決異步ThreadLocal 無法共享數據】RequestContextHolder.setRequestAttributes(requestAttributes);
5.2.2.5 模擬運費效果

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/WareInfoService.java

    /*** 獲取運費和收貨地址信息* @param addrId* @return*/FareVo getFare(Long addrId);

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/impl/WareInfoServiceImpl.java

模擬運費,真實情況下需要計算得出

    /*** 計算運費* @param addrId* @return*/@Overridepublic FareVo getFare(Long addrId) {FareVo fareVo = new FareVo();//收獲地址的詳細信息R addrInfo = memberFeignService.info(addrId);MemberAddressVo memberAddressVo = addrInfo.getData("memberReceiveAddress",new TypeReference<MemberAddressVo>() {});if (memberAddressVo != null) {String phone = memberAddressVo.getPhone();//截取用戶手機號碼最后一位作為我們的運費計算//1558022051String fare = phone.substring(phone.length() - 1);BigDecimal bigDecimal = new BigDecimal(fare);fareVo.setFare(bigDecimal);fareVo.setAddress(memberAddressVo);return fareVo;}return null;}

需要獲取用戶選擇的地址信息(遠程調用)

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/feign/MemberFeignService.java

    /*** 根據id獲取用戶地址信息* @param id* @return*/@RequestMapping("/member/memberreceiveaddress/info/{id}")R info(@PathVariable("id") Long id);

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/controller/WareInfoController.java

    /*** 獲取運費信息,訂單服務遠程調用* @return*/@GetMapping(value = "/fare")public R getFare(@RequestParam("addrId") Long addrId) {FareVo fare = wareInfoService.getFare(addrId);return R.ok().setData(fare);}

前端頁面當用戶地址切換時,查詢出運費及訂單總金額為用戶展示

測試需要創建兩條地址信息數據

5.2.2.6 創建防重令牌

令牌前綴常量

package site.zhourui.gulimall.order.constant;/*** @author zr* @date 2021/12/23 22:05*/
public class OrderConstant {public static final String USER_ORDER_TOKEN_PREFIX = "order:token";}

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

        //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);
5.2.2.7 提交訂單

注意:創建的訂單號很長,注意將oms_orderoms_order_item數據庫表中的order_sn字段調大至50,否則會報錯

  • 下單:去創建訂單,驗令牌,驗價格,鎖庫存
  • 提交訂單成功,則攜帶返回數據轉發至支付頁面
  • 提交訂單失敗,則攜帶錯誤信息重定向至確認頁

gulimall-order/src/main/java/site/zhourui/gulimall/order/web/OrderWebController.java

    @RequestMapping("/submitOrder")public String submitOrder(OrderSubmitVo submitVo, Model model, RedirectAttributes attributes) {try{SubmitOrderResponseVo responseVo=orderService.submitOrder(submitVo);Integer code = responseVo.getCode();if (code==0){model.addAttribute("order", responseVo.getOrder());return "pay";}else {String msg = "下單失敗;";switch (code) {case 1:msg += "防重令牌校驗失敗";break;case 2:msg += "商品價格發生變化";break;}attributes.addFlashAttribute("msg", msg);return "redirect:http://order.gulimall.com/toTrade";}}catch (Exception e){if (e instanceof NoStockException){String msg = "下單失敗,商品無庫存";attributes.addFlashAttribute("msg", msg);}return "redirect:http://order.gulimall.com/toTrade";}}

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

    /*** 提交訂單* @param vo* @return*/// @Transactional(isolation = Isolation.READ_COMMITTED) 設置事務的隔離級別// @Transactional(propagation = Propagation.REQUIRED)   設置事務的傳播級別// @GlobalTransactional(rollbackFor = Exception.class)@Transactional@Overridepublic SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {confirmVoThreadLocal.set(vo);SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();//去創建、下訂單、驗令牌、驗價格、鎖定庫存...//獲取當前用戶登錄的信息MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();responseVo.setCode(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();//通過lure腳本原子驗證令牌和刪除令牌Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()),orderToken);if (result == 0L) {//令牌驗證失敗responseVo.setCode(1);return responseVo;} else {//令牌驗證成功//1、創建訂單、訂單項等信息OrderCreateTo order = createOrder();//2、驗證價格BigDecimal payAmount = order.getOrder().getPayAmount();BigDecimal payPrice = vo.getPayPrice();if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {//金額對比//TODO 3、保存訂單saveOrder(order);//4、庫存鎖定,只要有異常,回滾訂單數據//訂單號、所有訂單項信息(skuId,skuNum,skuName)WareSkuLockVo lockVo = new WareSkuLockVo();lockVo.setOrderSn(order.getOrder().getOrderSn());//獲取出要鎖定的商品數據信息【order里面存儲的是Entity】List<OrderItemVo> orderItemVos = order.getOrderItems().stream().map((item) -> {OrderItemVo orderItemVo = new OrderItemVo();orderItemVo.setSkuId(item.getSkuId());orderItemVo.setCount(item.getSkuQuantity());orderItemVo.setTitle(item.getSkuName());return orderItemVo;}).collect(Collectors.toList());lockVo.setLocks(orderItemVos);//TODO 調用遠程鎖定庫存的方法//出現的問題:扣減庫存成功了,但是由于網絡原因超時,出現異常,導致訂單事務回滾,庫存事務不回滾(解決方案:seata)//為了保證高并發,不推薦使用seata,因為是加鎖,并行化,提升不了效率,可以發消息給庫存服務R r = wmsFeignService.orderLockStock(lockVo);if (r.getCode() == 0) {//鎖定成功responseVo.setOrder(order.getOrder());//int i = 10/0;//TODO 訂單創建成功,發送消息給MQ
//                    rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());//刪除購物車里的數據
//                    redisTemplate.delete(CART_PREFIX+memberResponseVo.getId());return responseVo;} else {//鎖定失敗String msg = (String) r.get("msg");throw new NoStockException(msg);// responseVo.setCode(3);// return responseVo;}} else {responseVo.setCode(2);return responseVo;}}}
5.2.2.7.1 驗證防重令牌
        //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();//通過lure腳本原子驗證令牌和刪除令牌Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()),orderToken);if (result == 0L) {//令牌驗證失敗responseVo.setCode(1);return responseVo;} else {//令牌驗證成功//1、創建訂單、訂單項等信息//2、驗證價格//3、保存訂單//4、庫存鎖定,只要有異常,回滾訂單數據}
5.2.2.7.2 創建訂單、訂單項等信息

需要遠程調用獲取SPU信息

gulimall-order/src/main/java/site/zhourui/gulimall/order/feign/ProductFeignService.java

package site.zhourui.gulimall.order.feign;import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import site.zhourui.common.utils.R;/*** @author zr* @date 2021/12/24 9:58*/
@FeignClient("gulimall-product")
public interface ProductFeignService {/*** 根據skuId查詢spu的信息* @param skuId* @return*/@GetMapping(value = "/product/spuinfo/skuId/{skuId}")R getSpuInfoBySkuId(@PathVariable("skuId") Long skuId);}

gulimall-product/src/main/java/site/zhourui/gulimall/product/app/SpuInfoController.java

    /*** 提交訂單,遠程接口* 根據skuId查詢spu的信息*/@GetMapping(value = "/skuId/{skuId}")public R getSpuInfoBySkuId(@PathVariable("skuId") Long skuId) {SpuInfoEntity spuInfoEntity = spuInfoService.getSpuInfoBySkuId(skuId);return R.ok().setData(spuInfoEntity);}

gulimall-product/src/main/java/site/zhourui/gulimall/product/service/SpuInfoService.java

    /*** 根據skuId查詢spu的信息* @param skuId* @return*/SpuInfoEntity getSpuInfoBySkuId(Long skuId);

gulimall-product/src/main/java/site/zhourui/gulimall/product/service/impl/SpuInfoServiceImpl.java

    /*** 根據skuId查詢spu的信息* @param skuId* @return*/@Overridepublic SpuInfoEntity getSpuInfoBySkuId(Long skuId) {//先查詢sku表里的數據SkuInfoEntity skuInfoEntity = skuInfoService.getById(skuId);//獲得spuIdLong spuId = skuInfoEntity.getSpuId();//再通過spuId查詢spuInfo信息表里的數據SpuInfoEntity spuInfoEntity = baseMapper.selectById(spuId);//查詢品牌表的數據獲取品牌名BrandEntity brandEntity = brandService.getById(spuInfoEntity.getBrandId());spuInfoEntity.setBrandName(brandEntity.getName());return spuInfoEntity;}

gulimall-product/src/main/java/site/zhourui/gulimall/product/entity/SpuInfoEntity.java

需要加上品牌名稱

	/*** 品牌名*/@TableField(exist = false)private String brandName;

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

    /*** 創建訂單*/private OrderCreateTo createOrder() {OrderCreateTo createTo = new OrderCreateTo();//1、生成訂單號String orderSn = IdWorker.getTimeId();// 構建訂單數據【封裝價格】OrderEntity orderEntity = builderOrder(orderSn);//2、獲取到所有的訂單項【封裝價格】List<OrderItemEntity> orderItemEntities = builderOrderItems(orderSn);//3、驗價(計算價格、積分等信息)computePrice(orderEntity, orderItemEntities);createTo.setOrder(orderEntity);createTo.setOrderItems(orderItemEntities);return createTo;}

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

    /*** 構建訂單數據* @param orderSn* @return*/private OrderEntity builderOrder(String orderSn) {//獲取當前用戶登錄信息MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();OrderEntity orderEntity = new OrderEntity();orderEntity.setMemberId(memberResponseVo.getId());orderEntity.setOrderSn(orderSn);orderEntity.setMemberUsername(memberResponseVo.getUsername());OrderSubmitVo orderSubmitVo = confirmVoThreadLocal.get();//遠程獲取收貨地址和運費信息R fareAddressVo = wmsFeignService.getFare(orderSubmitVo.getAddrId());FareVo fareResp = fareAddressVo.getData("data", new TypeReference<FareVo>() {});//獲取到運費信息BigDecimal fare = fareResp.getFare();orderEntity.setFreightAmount(fare);//獲取到收貨地址信息MemberAddressVo address = fareResp.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.setAutoConfirmDay(7);orderEntity.setConfirmStatus(0);return orderEntity;}

訂單狀態枚舉

gulimall-order/src/main/java/site/zhourui/gulimall/order/enume/OrderStatusEnum.java

package site.zhourui.gulimall.order.enume;/*** @author zr* @date 2021/12/24 9:52*//*** 訂單狀態枚舉*/public enum OrderStatusEnum {CREATE_NEW(0,"待付款"),PAYED(1,"已付款"),SENDED(2,"已發貨"),RECIEVED(3,"已完成"),CANCLED(4,"已取消"),SERVICING(5,"售后中"),SERVICED(6,"售后完成");private Integer code;private String msg;OrderStatusEnum(Integer code, String msg) {this.code = code;this.msg = msg;}public Integer getCode() {return code;}public String getMsg() {return msg;}
}

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

    /*** 構建所有訂單項數據* @return*/public List<OrderItemEntity> builderOrderItems(String orderSn) {List<OrderItemEntity> orderItemEntityList = new ArrayList<>();//最后確定每個購物項的價格List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();if (currentCartItems != null && currentCartItems.size() > 0) {orderItemEntityList = currentCartItems.stream().map((items) -> {//構建訂單項數據OrderItemEntity orderItemEntity = builderOrderItem(items);orderItemEntity.setOrderSn(orderSn);return orderItemEntity;}).collect(Collectors.toList());}return orderItemEntityList;}
5.2.2.7.3 驗價

將頁面提交的價格和后臺計算的價格進行對比,若不同則提示用戶商品價格發生變化

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

    /*** 計算價格的方法* @param orderEntity* @param orderItemEntities*/private void computePrice(OrderEntity orderEntity, List<OrderItemEntity> orderItemEntities) {//總價BigDecimal total = new BigDecimal("0.0");//優惠價BigDecimal coupon = new BigDecimal("0.0");BigDecimal intergration = new BigDecimal("0.0");BigDecimal promotion = new BigDecimal("0.0");//積分、成長值Integer integrationTotal = 0;Integer growthTotal = 0;//訂單總額,疊加每一個訂單項的總額信息for (OrderItemEntity orderItem : orderItemEntities) {//優惠價格信息coupon = coupon.add(orderItem.getCouponAmount());promotion = promotion.add(orderItem.getPromotionAmount());intergration = intergration.add(orderItem.getIntegrationAmount());//總價total = total.add(orderItem.getRealAmount());//積分信息和成長值信息integrationTotal += orderItem.getGiftIntegration();growthTotal += orderItem.getGiftGrowth();}//1、訂單價格相關的orderEntity.setTotalAmount(total);//設置應付總額(總額+運費)orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount()));orderEntity.setCouponAmount(coupon);orderEntity.setPromotionAmount(promotion);orderEntity.setIntegrationAmount(intergration);//設置積分成長值信息orderEntity.setIntegration(integrationTotal);orderEntity.setGrowth(growthTotal);//設置刪除狀態(0-未刪除,1-已刪除)orderEntity.setDeleteStatus(0);}
5.2.2.7.4 保存訂單

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

    /*** 保存訂單所有數據*/private void saveOrder(OrderCreateTo orderCreateTo) {//獲取訂單信息OrderEntity order = orderCreateTo.getOrder();order.setModifyTime(new Date());order.setCreateTime(new Date());//保存訂單this.baseMapper.insert(order);//獲取訂單項信息List<OrderItemEntity> orderItems = orderCreateTo.getOrderItems();//批量保存訂單項數據orderItemService.saveBatch(orderItems);}
5.2.2.7.5 庫存鎖定

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

                //TODO 調用遠程鎖定庫存的方法//出現的問題:扣減庫存成功了,但是由于網絡原因超時,出現異常,導致訂單事務回滾,庫存事務不回滾(解決方案:seata)//為了保證高并發,不推薦使用seata,因為是加鎖,并行化,提升不了效率,可以發消息給庫存服務R r = wmsFeignService.orderLockStock(lockVo);

gulimall-order/src/main/java/site/zhourui/gulimall/order/feign/WmsFeignService.java

  • 找出所有庫存大于商品數的倉庫
  • 遍歷所有滿足條件的倉庫,逐個嘗試鎖庫存,若鎖庫存成功則退出遍歷
    /*** 鎖定庫存*/@PostMapping(value = "/ware/waresku/lock/order")R orderLockStock(@RequestBody WareSkuLockVo vo);

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/controller/WareSkuController.java

    /*** 下訂單時鎖庫存* @param* @return*/@RequestMapping("/lock/order")public R orderLockStock(@RequestBody WareSkuLockVo lockVo) {try {Boolean lock = wareSkuService.orderLockStock(lockVo);return R.ok();} catch (NoStockException e) {return R.error(BizCodeEnume.NO_STOCK_EXCEPTION.getCode(), BizCodeEnume.NO_STOCK_EXCEPTION.getMsg());}}

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/WareSkuService.java

    /*** 鎖定庫存*/boolean orderLockStock(WareSkuLockVo vo);

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/impl/WareSkuServiceImpl.java

    /*** 為某個訂單鎖定庫存*/@Transactional(rollbackFor = Exception.class)@Overridepublic boolean orderLockStock(WareSkuLockVo vo) {/*** 保存庫存工作單詳情信息* 追溯* 如果沒有庫存,就不會發送消息給mq* 【不會進入save(WareOrderTaskDetailEntity)邏輯,也不會發送消息給mq,也不會鎖定庫存,也不會監聽到解鎖服務】*/WareOrderTaskEntity wareOrderTaskEntity = new WareOrderTaskEntity();wareOrderTaskEntity.setOrderSn(vo.getOrderSn());wareOrderTaskEntity.setCreateTime(new Date());wareOrderTaskService.save(wareOrderTaskEntity);//1、按照下單的收貨地址,找到一個就近倉庫,鎖定庫存//2、找到每個商品在哪個倉庫都有庫存List<OrderItemVo> locks = vo.getLocks();List<SkuWareHasStock> collect = locks.stream().map((item) -> {SkuWareHasStock stock = new SkuWareHasStock();Long skuId = item.getSkuId();stock.setSkuId(skuId);stock.setNum(item.getCount());//查詢這個商品在哪個倉庫有庫存 stock-鎖定num > 0List<Long> wareIdList = wareSkuDao.listWareIdHasSkuStock(skuId);stock.setWareId(wareIdList);return stock;}).collect(Collectors.toList());//2、鎖定庫存for (SkuWareHasStock hasStock : collect) {boolean skuStocked = false;Long skuId = hasStock.getSkuId();List<Long> wareIds = hasStock.getWareId();if (CollectionUtils.isEmpty(wareIds)) {//沒有任何倉庫有這個商品的庫存throw new NoStockException(skuId);}//1、如果每一個商品都鎖定成功,將當前商品鎖定了幾件的工作單記錄發給MQ//2、鎖定失敗。前面保存的工作單信息都回滾了。發送出去的消息,即使要解鎖庫存,由于在數據庫查不到指定的id,所有就不用解鎖for (Long wareId : wareIds) {//鎖定成功就返回1,失敗就返回0Long count = wareSkuDao.lockSkuStock(skuId,wareId,hasStock.getNum());// count==1表示鎖定成功if (count == 1) {skuStocked = true;
//                    WareOrderTaskDetailEntity taskDetailEntity = WareOrderTaskDetailEntity.builder()
//                            .skuId(skuId)
//                            .skuName("")
//                            .skuNum(hasStock.getNum())
//                            .taskId(wareOrderTaskEntity.getId())
//                            .wareId(wareId)
//                            .lockStatus(1)
//                            .build();
//                    wareOrderTaskDetailService.save(taskDetailEntity);
//
//                    //TODO 告訴MQ庫存鎖定成功
//                    StockLockedTo lockedTo = new StockLockedTo();
//                    lockedTo.setId(wareOrderTaskEntity.getId());
//                    StockDetailTo detailTo = new StockDetailTo();
//                    BeanUtils.copyProperties(taskDetailEntity,detailTo);// 這里直接存entity。但是應該存id更好,數據最好來自DB
//                    lockedTo.setDetailTo(detailTo);
//                    rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",lockedTo);// 鎖定成功返回break;} else {//當前倉庫鎖失敗,重試下一個倉庫}}if (skuStocked == false) {//當前商品所有倉庫都沒有鎖住throw new NoStockException(skuId);}}//3、肯定全部都是鎖定成功的return true;}
5.2.2.8 訂單提交的問題 (本地事務在分布式情況下出現的問題)

分布式情況下,可能出現一些服務事務不一致的情況

  • 遠程服務假失敗
  • 遠程服務執行完成后,下面其他方法出現異常

庫存扣減成功但是訂單業務執行出錯,訂單業務可以回滾但遠程調用的庫存服務是辦法回滾的

5.2.2.9 使用seata解決分布式事務

有多種模式:AT、TCC、SAGA 和 XA
doc:http://seata.io/zh-cn/docs/overview/what-is-seata.html

1、TC不會控制各RM回滾,而是調用補償方案,AT模式是根據 回滾日志表【每個數據庫都創建一個回滾日志表】
2、而TCC模式的回滾是根據補償方法 來回滾

AT模式:Auto Transiaction:自動事務模式,根據回滾日志表自動回滾
TCC模式:就是根據自己手寫的事務補償方法 來回滾

Seata術語

TC (Transaction Coordinator) - 事務協調者
維護全局和分支事務的狀態,驅動全局事務提交或回滾。

TM (Transaction Manager) - 事務管理器
定義全局事務的范圍:開始全局事務、提交或回滾全局事務。

RM (Resource Manager) - 資源管理器
管理分支事務處理的資源,與TC交談以注冊分支事務和報告分支事務的狀態,并驅動分支事務提交或回滾。

5.2.2.9.1 AT模式實現步驟【創建訂單+鎖定庫存】【不推薦使用】

不適用高并發場景,適用于商品服務,保存商品的那個接口 SpuInfoController
/save
1、保存spu pms_spu_info
2、保存attr
3、保存描述圖片 pms_spu_info_desc
4、保存圖片集 pms_spu_images
5、保存當前spu對應的所有sku信息
6、優惠券信息【遠程調用】分布式事務【并發不高,可以使用AT模式,@GlobalTransactional】
7、保存積分信息【遠程調用】分布式事務【并發不高,可以使用AT模式,@GlobalTransactional】

seata官方文檔:https://github.com/seata/seata-samples/blob/master/doc/quick-integration-with-spring-cloud.md

1.創建 UNDO_LOG 表

-- 注意此處0.3.0+ 增加唯一索引 ux_undo_log
CREATE TABLE `undo_log` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`branch_id` bigint(20) NOT NULL,`xid` varchar(100) NOT NULL,`context` varchar(128) NOT NULL,`rollback_info` longblob NOT NULL,`log_status` int(11) NOT NULL,`log_created` datetime NOT NULL,`log_modified` datetime NOT NULL,`ext` varchar(100) DEFAULT NULL,PRIMARY KEY (`id`),UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

2.導入依賴

        <!--seata 分布式事務--><!--不使用的模塊要排除掉--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId><exclusions><exclusion><groupId>io.seata</groupId><artifactId>seata-all</artifactId></exclusion></exclusions></dependency>
<!--        使用與事務協調器版本相同的--><dependency><groupId>io.seata</groupId><artifactId>seata-all</artifactId><version>0.7.1</version></dependency>

下載安裝事務協調器:seate-server0.7.1Release v0.7.1 · seata/seata · GitHub

版本與seata-all版本對應

3.配置seata的注冊中心 registry.conf

4.啟動?D:\environment\seata-server-0.7.1\bin\seata-server.bat

seata各種屬性配置 file.conf

5.所有想要用到分布式事務的微服務使用seata DataSourceProxy 代理自己的數據源

package site.zhourui.gulimall.order.config;/*** @author zr* @date 2021/12/28 10:57*/import com.zaxxer.hikari.HikariDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.util.StringUtils;import javax.sql.DataSource;/*** seata分布式事務* 配置代理數據源*/
//@Configuration
public class MySeataConfig {@AutowiredDataSourceProperties dataSourceProperties;/*** 自動配置類,如果容器中存在數據源就不自動配置數據源了*/@Beanpublic DataSource dataSource(DataSourceProperties dataSourceProperties) {HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();if (StringUtils.hasText(dataSourceProperties.getName())) {dataSource.setPoolName(dataSourceProperties.getName());}return new DataSourceProxy(dataSource);}
}

6.每個使用分布式事務的微服務都需要導入 file.conf registry.conf

注意file.conf:

需要注意的是 service.vgroup_mapping這個配置,在 Spring Cloud 中默認是${spring.application.name}-fescar-service-group ,可以通過指定application.propertiesspring.cloud.alibaba.seata.tx-service-group這個屬性覆蓋,但是必須要和 file.conf中的一致,否則會提示 no available server to connect

7.加注解

  • 給分布式大事務的入口標注@GlobalTransactional gulimall-order服務
  •  每一個遠程的小事務用@Trabsactional                     gulimall-ware服務
    

重啟服務測試

測試完成后關閉seataGlobalTransactional,排除依賴 gulimall-order,gulimall-ware

5.2.2.10 最終一致性庫存解鎖邏輯:基于消息隊列的分布式事務+分布式表【庫存自動解鎖】

5.2.2.10.1 為庫存模塊創建業務交換機,隊列,綁定(整合Rabbitmq)

導入依賴

gulimall-ware/pom.xml

        <!--rabbitmq--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId></dependency>

配置Rabbitmq地址端口(虛擬主機,確認發送,抵達確認,手動ack)

spring:rabbitmq:host: 192.168.157.128port: 5672virtual-host: /#開啟發送端確認publisher-confirms: true# 開啟發送端消息抵達Queue確認publisher-returns: true# 只要消息抵達Queue,就會異步發送優先回調returnfirmtemplate:mandatory: true#    使用手動ack確認模式,關閉自動確認【消息丟失】listener:simple:acknowledge-mode: manual

開啟RabbitMQ

主啟動類加上

@EnableRabbit

配置確認回調,失敗回調

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/config/MyRabbitConfig.java

package site.zhourui.gulimall.ware.config;import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import javax.annotation.PostConstruct;/*** @author zr* @date 2021/12/15 9:56*/
@Configuration
public class MyRabbitConfig {@AutowiredRabbitTemplate rabbitTemplate;@Beanpublic MessageConverter messageConverter(){return new Jackson2JsonMessageConverter();}/*** 定制RabbitTemplate* 1、服務收到消息就會回調*      1、spring.rabbitmq.publisher-confirms: true*      2、設置確認回調* 2、消息正確抵達隊列就會進行回調*      1、spring.rabbitmq.publisher-returns: true*         spring.rabbitmq.template.mandatory: true*      2、設置確認回調ReturnCallback** 3、消費端確認(保證每個消息都被正確消費,此時才可以broker刪除這個消息)**/@PostConstruct  //MyRabbitConfig對象創建完成以后,執行這個方法public void initRabbitTemplate() {/*** 1、只要消息抵達Broker就ack=true* correlationData:當前消息的唯一關聯數據(這個是消息的唯一id)* ack:消息是否成功收到* cause:失敗的原因*///設置確認回調rabbitTemplate.setConfirmCallback((correlationData,ack,cause) -> {System.out.println("confirm...correlationData["+correlationData+"]==>ack:["+ack+"]==>cause:["+cause+"]");});/*** 只要消息沒有投遞給指定的隊列,就觸發這個失敗回調* message:投遞失敗的消息詳細信息* replyCode:回復的狀態碼* replyText:回復的文本內容* exchange:當時這個消息發給哪個交換機* routingKey:當時這個消息用哪個路郵鍵*/rabbitTemplate.setReturnCallback((message,replyCode,replyText,exchange,routingKey) -> {System.out.println("Fail Message["+message+"]==>replyCode["+replyCode+"]" +"==>replyText["+replyText+"]==>exchange["+exchange+"]==>routingKey["+routingKey+"]");});}
}

創建庫存解鎖延時隊列及交換機,綁定

package site.zhourui.gulimall.ware.config;/*** @author zr* @date 2021/12/28 16:47*/import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.io.IOException;
import java.util.HashMap;/*** 創建隊列,交換機,延遲隊列,綁定關系 的configuration* 不會重復創建覆蓋* 1、第一次使用隊列【監聽】的時候才會創建* 2、Broker沒有隊列、交換機才會創建*/
@Configuration
public class MyRabbitMQConfig {@RabbitListener(queues = "stock.release.stock.queue")public void listen( Channel channel, Message message) throws IOException {System.out.println("收到庫存解鎖消息,準備解鎖庫存:------>");channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);}/*** 庫存服務默認的交換機* @return*/@Beanpublic Exchange stockEventExchange() {//String name, boolean durable, boolean autoDelete, Map<String, Object> argumentsTopicExchange topicExchange = new TopicExchange("stock-event-exchange", true, false);return topicExchange;}/*** 普通隊列* @return*/@Beanpublic Queue stockReleaseStockQueue() {//String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> argumentsQueue queue = new Queue("stock.release.stock.queue", true, false, false);return queue;}/*** 延遲隊列* @return*/@Beanpublic Queue stockDelay() {HashMap<String, Object> arguments = new HashMap<>();arguments.put("x-dead-letter-exchange", "stock-event-exchange");arguments.put("x-dead-letter-routing-key", "stock.release");// 消息過期時間 2分鐘arguments.put("x-message-ttl", 120000);Queue queue = new Queue("stock.delay.queue", true, false, false,arguments);return queue;}/*** 交換機與普通隊列綁定* @return*/@Beanpublic Binding stockLocked() {//String destination, DestinationType destinationType, String exchange, String routingKey,// 			Map<String, Object> argumentsBinding binding = new Binding("stock.release.stock.queue",Binding.DestinationType.QUEUE,"stock-event-exchange","stock.release.#",null);return binding;}/*** 交換機與延遲隊列綁定* @return*/@Beanpublic Binding stockLockedBinding() {return new Binding("stock.delay.queue",Binding.DestinationType.QUEUE,"stock-event-exchange","stock.locked",null);}}

啟動測試

stock.locked路郵鍵發送隊列,并監聽死信隊列,兩分鐘后監聽到消息說明成功了

5.2.2.10.2 庫存解鎖

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/WareSkuService.java

    /*** 解鎖庫存* @param to*/void unlockStock(StockLockedTo to);

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/impl/WareSkuServiceImpl.java

此處需要調用訂單服務遠程服務根據訂單號查詢訂單信息

    /*** 解鎖庫存*/@Overridepublic void unlockStock(StockLockedTo to) {//庫存工作單的idStockDetailTo detail = to.getDetailTo();Long detailId = detail.getId();/*** 解鎖* 1、查詢數據庫關于這個訂單鎖定庫存信息*   有:證明庫存鎖定成功了*      解鎖:訂單狀況*          1、沒有這個訂單,必須解鎖庫存*          2、有這個訂單,不一定解鎖庫存*              訂單狀態:已取消:解鎖庫存*                      已支付:不能解鎖庫存*/WareOrderTaskDetailEntity taskDetailInfo = wareOrderTaskDetailService.getById(detailId);if (taskDetailInfo != null) {//查出wms_ware_order_task工作單的信息Long id = to.getId();WareOrderTaskEntity orderTaskInfo = wareOrderTaskService.getById(id);//獲取訂單號查詢訂單狀態String orderSn = orderTaskInfo.getOrderSn();//遠程查詢訂單信息R orderData = orderFeignService.getOrderStatus(orderSn);if (orderData.getCode() == 0) {//訂單數據返回成功OrderVo orderInfo = orderData.getData("data", new TypeReference<OrderVo>() {});//判斷訂單狀態是否已取消或者支付或者訂單不存在// 1、訂單不存在:解鎖// 2、訂單存在,且訂單狀態是取消狀態:解鎖if (orderInfo == null || orderInfo.getStatus() == 4) {// 工作單狀態必須是 已鎖定 才可以解鎖【因為解鎖方法沒有加事務】if (taskDetailInfo.getLockStatus() == 1) {unLockStock(detail.getSkuId(),detail.getWareId(),detail.getSkuNum(),detailId);}}} else {//消息拒絕以后重新放在隊列里面,讓別人繼續消費解鎖//遠程調用服務失敗throw new RuntimeException("遠程調用服務失敗");}} else {//無需解鎖【回滾狀態】}}

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/feign/OrderFeignService.java

package site.zhourui.gulimall.ware.feign;import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import site.zhourui.common.utils.R;/*** @author zr* @date 2021/12/29 15:29*/
@FeignClient("gulimall-order")
public interface OrderFeignService {@GetMapping(value = "/order/order/status/{orderSn}")R getOrderStatus(@PathVariable("orderSn") String orderSn);}

遠程訂單服務

gulimall-order/src/main/java/site/zhourui/gulimall/order/controller/OrderController.java

    /*** 根據訂單編號查詢訂單狀態* @param orderSn* @return*/@GetMapping(value = "/status/{orderSn}")public R getOrderStatus(@PathVariable("orderSn") String orderSn) {OrderEntity orderEntity = orderService.getOrderByOrderSn(orderSn);return R.ok().setData(orderEntity);}

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/OrderService.java

    /*** 按照訂單號獲取訂單信息* @param orderSn* @return*/OrderEntity getOrderByOrderSn(String orderSn);

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

    /*** 按照訂單號獲取訂單信息* @param orderSn* @return*/@Overridepublic OrderEntity getOrderByOrderSn(String orderSn) {OrderEntity orderEntity = this.baseMapper.selectOne(new QueryWrapper<OrderEntity>().eq("order_sn", orderSn));return orderEntity;}
5.2.2.10.3 監聽庫存解鎖

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/entity/WareOrderTaskDetailEntity.java

增加兩個字段 倉庫id,鎖定狀態使用@Builder

package site.zhourui.gulimall.ware.entity;import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;import java.io.Serializable;
import java.util.Date;import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;/*** 庫存工作單** @author zr* @email 2437264464@qq.com* @date 2021-09-28 15:47:50*/
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
@TableName("wms_ware_order_task_detail")
public class WareOrderTaskDetailEntity implements Serializable {private static final long serialVersionUID = 1L;/*** 倉庫id*/private Long wareId;/*** 鎖定狀態* 1-已鎖定 2-已解鎖 3-已扣減*/private Integer lockStatus;/*** id*/@TableIdprivate Long id;/*** sku_id*/private Long skuId;/*** sku_name*/private String skuName;/*** 購買個數*/private Integer skuNum;/*** 工作單id*/private Long taskId;}

鎖定庫存時向庫存延時隊列發送一條庫存工作單記錄

庫存工作單

gulimall-common/src/main/java/site/zhourui/common/to/mq/StockLockedTo.java

package site.zhourui.common.to.mq;/***  鎖定庫存成功,往延時隊列存入 工作單to 對象*  wms_ware_order_task* @author zr* @date 2021/12/29 15:07*/import lombok.Data;/***/
@Data
public class StockLockedTo {/** 庫存工作單的id **/private Long id;/** 庫存單詳情 wms_ware_order_task_detail**/private StockDetailTo detailTo;
}

庫存詳情單

gulimall-common/src/main/java/site/zhourui/common/to/mq/StockDetailTo.java

package site.zhourui.common.to.mq;/*** 庫存單詳情* wms_ware_order_task_detail* @author zr* @date 2021/12/29 15:07*/import lombok.Data;@Data
public class StockDetailTo {private Long id;/*** sku_id*/private Long skuId;/*** sku_name*/private String skuName;/*** 購買個數*/private Integer skuNum;/*** 工作單id*/private Long taskId;/*** 倉庫id*/private Long wareId;/*** 鎖定狀態* 1-鎖定 2-解鎖 3-扣減*/private Integer lockStatus;}

監聽庫存死信隊列,解鎖庫存

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/listener/StockReleaseListener.java

package site.zhourui.gulimall.ware.listener;import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import site.zhourui.common.to.mq.StockLockedTo;
import site.zhourui.gulimall.ware.service.WareSkuService;import java.io.IOException;/*** 監聽死信隊列,解鎖庫存* @author zr* @date 2021/12/29 15:22*/
@Slf4j
@RabbitListener(queues = "stock.release.stock.queue")
@Service
public class StockReleaseListener {@Autowiredprivate WareSkuService wareSkuService;/*** 這個是監聽死信消息* 1、庫存自動解鎖*  下訂單成功,庫存鎖定成功,接下來的業務調用失敗,導致訂單回滾。之前鎖定的庫存就要自動解鎖**  2、訂單失敗*      庫存鎖定失敗**   只要解鎖庫存的消息失敗,一定要告訴服務解鎖失敗*/@RabbitHandlerpublic void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {System.out.println("******收到解鎖庫存的延時信息******,準備解鎖" + to.getDetailTo().getId());try {//當前消息是否被第二次及以后(重新)派發過來了// Boolean redelivered = message.getMessageProperties().getRedelivered();//解鎖庫存wareSkuService.unlockStock(to);// 手動刪除消息channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);} catch (Exception e) {// 解鎖失敗 將消息重新放回隊列,讓別人消費channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);}}/*** 客戶取消訂單,監聽到消息*/@RabbitHandlerpublic void handleOrderCloseRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {System.out.println("******收到訂單關閉,準備解鎖庫存的信息******訂單號:" + orderTo.getOrderSn());try {wareSkuService.unlockStock(orderTo);// 手動刪除消息channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);} catch (Exception e) {// 解鎖失敗 將消息重新放回隊列,讓別人消費channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);}}
}

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/WareSkuService.java

    /*** 解鎖庫存* @param to*/void unlockStock(StockLockedTo to);

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/impl/WareSkuServiceImpl.java

此處需要調用訂單服務遠程查詢訂單信息

    /*** 解鎖庫存*/@Overridepublic void unlockStock(StockLockedTo to) {//庫存工作單的idStockDetailTo detail = to.getDetailTo();Long detailId = detail.getId();/*** 解鎖* 1、查詢數據庫關于這個訂單鎖定庫存信息*   有:證明庫存鎖定成功了*      解鎖:訂單狀況*          1、沒有這個訂單,必須解鎖庫存*          2、有這個訂單,不一定解鎖庫存*              訂單狀態:已取消:解鎖庫存*                      已支付:不能解鎖庫存*/WareOrderTaskDetailEntity taskDetailInfo = wareOrderTaskDetailService.getById(detailId);if (taskDetailInfo != null) {//查出wms_ware_order_task工作單的信息Long id = to.getId();WareOrderTaskEntity orderTaskInfo = wareOrderTaskService.getById(id);//獲取訂單號查詢訂單狀態String orderSn = orderTaskInfo.getOrderSn();//遠程查詢訂單信息R orderData = orderFeignService.getOrderStatus(orderSn);if (orderData.getCode() == 0) {//訂單數據返回成功OrderVo orderInfo = orderData.getData("data", new TypeReference<OrderVo>() {});//判斷訂單狀態是否已取消或者支付或者訂單不存在// 1、訂單不存在:解鎖// 2、訂單存在,且訂單狀態是取消狀態:解鎖if (orderInfo == null || orderInfo.getStatus() == 4) {// 工作單狀態必須是 已鎖定 才可以解鎖【因為解鎖方法沒有加事務】if (taskDetailInfo.getLockStatus() == 1) {unLockStock(detail.getSkuId(),detail.getWareId(),detail.getSkuNum(),detailId);}}} else {//消息拒絕以后重新放在隊列里面,讓別人繼續消費解鎖//遠程調用服務失敗throw new RuntimeException("遠程調用服務失敗");}} else {//無需解鎖【回滾狀態】}}/*** 解鎖庫存的方法【設計DB,沒加事務】*/public void unLockStock(Long skuId,Long wareId,Integer num,Long taskDetailId) {// 1、庫存解鎖wareSkuDao.unLockStock(skuId,wareId,num);// 2、更新工作單的狀態 為已解鎖 2WareOrderTaskDetailEntity taskDetailEntity = new WareOrderTaskDetailEntity();taskDetailEntity.setId(taskDetailId);taskDetailEntity.setLockStatus(2);wareOrderTaskDetailService.updateById(taskDetailEntity);}

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/feign/OrderFeignService.java

遠程查詢訂單信息

package site.zhourui.gulimall.ware.feign;import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import site.zhourui.common.utils.R;/*** @author zr* @date 2021/12/29 15:29*/
@FeignClient("gulimall-order")
public interface OrderFeignService {@GetMapping(value = "/order/order/status/{orderSn}")R getOrderStatus(@PathVariable("orderSn") String orderSn);}

gulimall-order/src/main/java/site/zhourui/gulimall/order/controller/OrderController.java

    /*** 根據訂單編號查詢訂單狀態* @param orderSn* @return*/@GetMapping(value = "/status/{orderSn}")public R getOrderStatus(@PathVariable("orderSn") String orderSn) {OrderEntity orderEntity = orderService.getOrderByOrderSn(orderSn);return R.ok().setData(orderEntity);}

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/OrderService.java

    /*** 按照訂單號獲取訂單信息* @param orderSn* @return*/OrderEntity getOrderByOrderSn(String orderSn);

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

    /*** 按照訂單號獲取訂單信息* @param orderSn* @return*/@Overridepublic OrderEntity getOrderByOrderSn(String orderSn) {OrderEntity orderEntity = this.baseMapper.selectOne(new QueryWrapper<OrderEntity>().eq("order_sn", orderSn));return orderEntity;}
5.2.2.10.4 遠程服務order訂單服務登錄攔截跳轉login.html

gulimall-order/src/main/java/site/zhourui/gulimall/order/interceptor/LoginUserInterceptor.java

將該請求放行

        String uri = request.getRequestURI();AntPathMatcher antPathMatcher = new AntPathMatcher();boolean match = antPathMatcher.match("/order/order/status/**", uri);return match;
5.2.2.11 最終一致性庫存解鎖邏輯:基于消息隊列的分布式事務+分布式表【訂單自動關單】
5.2.2.11.1 為訂單模塊創建業務交換機,隊列綁定

谷粒商城--消息隊列--高級篇筆記十6.6 延時隊列定時關單模擬已經創建了交換機隊列,綁定

https://blog.csdn.net/qq_31745863/article/details/122212434

5.2.2.11.2 訂單關閉

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/OrderService.java

    /*** 關閉訂單* @param orderEntity*/void closeOrder(OrderEntity orderEntity);

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

    /*** 關閉訂單*/@Overridepublic void closeOrder(OrderEntity orderEntity) {//關閉訂單之前先查詢一下數據庫,判斷此訂單狀態是否已支付OrderEntity orderInfo = this.getOne(new QueryWrapper<OrderEntity>().eq("order_sn",orderEntity.getOrderSn()));if (orderInfo.getStatus().equals(OrderStatusEnum.CREATE_NEW.getCode())) {//代付款狀態進行關單OrderEntity orderUpdate = new OrderEntity();orderUpdate.setId(orderInfo.getId());orderUpdate.setStatus(OrderStatusEnum.CANCLED.getCode());this.updateById(orderUpdate);// 發送消息給MQOrderTo orderTo = new OrderTo();BeanUtils.copyProperties(orderInfo, orderTo);try {//TODO 確保每個消息發送成功,給每個消息做好日志記錄,(給數據庫保存每一個詳細信息)保存每個消息的詳細信息//TODO 定期掃描數據庫,重新發送失敗的消息rabbitTemplate.convertAndSend("order-event-exchange", "order.release.other", orderTo);} catch (Exception e) {}}}
5.2.2.11.3 監聽訂單自動關單

gulimall-order/src/main/java/site/zhourui/gulimall/order/listener/OrderCloseListener.java

package site.zhourui.gulimall.order.interceptor;import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import site.zhourui.gulimall.order.entity.OrderEntity;
import site.zhourui.gulimall.order.service.OrderService;import java.io.IOException;/*** @author zr* @date 2021/12/29 17:22*/
/*** 定時關閉訂單**/
@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderCloseListener {@Autowiredprivate OrderService orderService;@RabbitHandlerpublic void listener(OrderEntity orderEntity, Channel channel, Message message) throws IOException {System.out.println("收到過期的訂單信息,準備關閉訂單" + orderEntity.getOrderSn());try {orderService.closeOrder(orderEntity);// 手動調用支付寶收單【這里省略了,可以參照demo中的代碼】channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);} catch (Exception e) {channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);}}}
5.2.2.12 測試自動關單,自動解鎖庫存

清空之前的訂單與庫存鎖定,庫存工作單(也可以不清,但需要記住提交此時的狀態,這樣好看一點)

清空mq消息

下單

mq的訂單延時隊列(1分鐘),庫存延時隊列(2分鐘)

一分鐘之內數據庫狀態

大于一分鐘小于兩分鐘,自動關單

其他數據庫表與一分鐘一致

大于兩分鐘,庫存自動解鎖

5.2.2.13 訂單卡頓導致的庫存無法解鎖

防止訂單服務卡頓,導致訂單狀態消息一直改不了,庫存優先到期,查訂單狀態新建,什么都不處理
導致卡頓的訂單,永遠都不能解鎖庫存

解決方案

再往訂單死信隊列發送消息時,同時也往庫存死信隊列發送相同消息,通知庫存解鎖

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/listener/StockReleaseListener.java

    /*** 客戶取消訂單,監聽到消息*/@RabbitHandlerpublic void handleOrderCloseRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {System.out.println("******收到訂單關閉,準備解鎖庫存的信息******訂單號:" + orderTo.getOrderSn());try {wareSkuService.unlockStock(orderTo);// 手動刪除消息channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);} catch (Exception e) {// 解鎖失敗 將消息重新放回隊列,讓別人消費channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);}}

重載解鎖庫存

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/WareSkuService.java

    /*** 解鎖訂單*/void unlockStock(OrderTo orderTo);

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/impl/WareSkuServiceImpl.java

    /*** 防止訂單服務卡頓,導致訂單狀態消息一直改不了,庫存優先到期,查訂單狀態新建,什么都不處理* 導致卡頓的訂單,永遠都不能解鎖庫存* @param orderTo*/@Transactional(rollbackFor = Exception.class)@Overridepublic void unlockStock(OrderTo orderTo) {String orderSn = orderTo.getOrderSn();//查一下最新的庫存解鎖狀態,防止重復解鎖庫存WareOrderTaskEntity orderTaskEntity = wareOrderTaskService.getOrderTaskByOrderSn(orderSn);//按照工作單的id找到所有 沒有解鎖的庫存,進行解鎖Long id = orderTaskEntity.getId();List<WareOrderTaskDetailEntity> list = wareOrderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>().eq("task_id", id).eq("lock_status", 1));for (WareOrderTaskDetailEntity taskDetailEntity : list) {unLockStock(taskDetailEntity.getSkuId(),taskDetailEntity.getWareId(),taskDetailEntity.getSkuNum(),taskDetailEntity.getId());}}

在庫存解鎖前查一下最新的庫存解鎖狀態,防止重復解鎖庫存

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/WareOrderTaskService.java

    WareOrderTaskEntity getOrderTaskByOrderSn(String orderSn);

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/impl/WareOrderTaskServiceImpl.java

    @Overridepublic WareOrderTaskEntity getOrderTaskByOrderSn(String orderSn) {WareOrderTaskEntity orderTaskEntity = this.baseMapper.selectOne(new QueryWrapper<WareOrderTaskEntity>().eq("order_sn", orderSn));return orderTaskEntity;}
5.2.2.14 如何保證消息可靠性
5.2.2.14.1 消息丟失
  • 消息發送出去,由于網絡問題沒有抵達服務器
    • 做好容錯方法(try-catch),發送消息可能會網絡失敗,失敗后要有重試機制,可記錄到數據庫,采用定期掃描重發的方式
    • 做好日志記錄,每個消息狀態是否都被服務器收到都應該記錄
    • 做好定期重發,如果消息沒有發送成功,定期去數據庫掃描未成功的消息進行重發
  • 消息抵達Broker,Broker要將消息寫入磁盤(持久化)才算成功。此時Broker尚未持久化完成,宕機。
    • publisher也必須加入確認回調機制,確認成功的消息,修改數據庫消息狀態。
  • 自動ACK的狀態下。消費者收到消息,但沒來得及消息然后宕機
    • 一定開啟手動ACK,消費成功才移除,失敗或者沒來得及處理就noAck并重新入隊
5.2.2.14.2 消息重復
  • 消息消費成功,事務已經提交,ack時,機器宕機。導致沒有ack成功,Broker的消息
    重新由unack變為ready,并發送給其他消費者

  • 消息消費失敗,由于重試機制,自動又將消息發送出去

  • 成功消費,ack時宕機,消息由unack變為ready,Broker又重新發送

    • 消費者的業務消費接口應該設計為冪等性的。比如扣庫存有工作單的狀態標志

    • 使用防重表(redis/mysql),發送消息每一個都有業務的唯一標識,處理過就不用處理

      CREATE TABLE `mq_message`(`message_id` char(32) not null ,`content` text, #json`to_exchange` char(255) default null ,`routing_key` char(255) default null ,`class_type` char(255) default null ,`message_status` int(1) default '0' comment '0-新建 1-已發送 2-錯誤抵達 3-已抵達',`create_time` datetime default null ,`update_time` datetime default null
      )
      

    • rabbitMQ的每一個消息都有redelivered字段,可以獲取是否是被重新投遞過來的,而不是第一次投遞過來的

5.2.2.14.3 消息積壓
  • 消費者宕機積壓
  • 消費者消費能力不足積壓
  • 發送者發送流量太大
    • 上線更多的消費者,進行正常消費
    • 上線專門的隊列消費服務,將消息先批量取出來,記錄數據庫,離線慢慢處理

5.3 訂單支付頁

5.3.1 加密

5.3.1.1 對稱加密

加密與解密用的秘鑰都是一樣的

5.3.1.2 非對稱加密

加密與解密用到的秘鑰不一致

5.3.2 支付寶加密原理

  • 支付寶加密采用RSA非對稱加密,分別在商戶端和支付寶端有兩對公鑰和私鑰
  • 在發送訂單數據時,直接使用明文,但會使用商戶私鑰加一個對應的簽名,支付寶端會使用商戶公鑰對簽名進行驗簽,只有數據明文和簽名對應的時候才能說明傳輸正確
  • 支付成功后,支付寶發送支付成功數據之外,還會使用支付寶私鑰加一個對應的簽名,商戶端收到支付成功數據之后也會使用支付寶公鑰延簽,成功后才能確認

5.3.2.1 什么是公鑰、 私鑰、 加密、 簽名和驗簽?
5.3.2.1.1 公鑰私鑰

公鑰和私鑰是一個相對概念
它們的公私性是相對于生成者來說的。
一對密鑰生成后, 保存在生成者手里的就是私鑰,生成者發布出去大家用的就是公鑰

5.3.2.1.2 加密和數字簽名
  • 加密是指:
    • 我們使用一對公私鑰中的一個密鑰來對數據進行加密, 而使用另一個密鑰來進行解密的技術。
    • 公鑰和私鑰都可以用來加密, 也都可以用來解密。
    • 但這個加解密必須是一對密鑰之間的互相加解密, 否則不能成功。
    • 加密的目的是:
      • 為了確保數據傳輸過程中的不可讀性, 就是不想讓別人看到。
  • 簽名:
    • 給我們將要發送的數據, 做上一個唯一簽名(類似于指紋)
    • 用來互相驗證接收方和發送方的身份;
    • 在驗證身份的基礎上再驗證一下傳遞的數據是否被篡改過。 因此使用數字簽名可以用來達到數據的明文傳輸。
  • 驗簽
    支付寶為了驗證請求的數據是否商戶本人發的,
    商戶為了驗證響應的數據是否支付寶發的

5.3.3 支付寶官方demo測試

官方demo下載地址:https://opendocs.alipay.com/open/54/106682

5.3.3.1 使用支付寶沙箱環境進行測試:https://open.alipay.com/platform/appDaily.htm?tab=account

5.3.3.2 自定義秘鑰,點擊rsa2秘鑰后面的設置并啟用

5.3.3.3 利用秘鑰工具生成自己的公鑰,私鑰,拿到支付寶公鑰

5.3.3.4 配置demo中的AlipayConfig

5.3.3.5 啟動demo

配置web目錄

添加archive

配置Tomcat

添加依賴

配置字符集

啟動Tomcat,測試

能夠字符成功的話就說明測試成功了

測試賬號密碼https://open.alipay.com/platform/appDaily.htm?tab=account

5.3.4 支付寶支付流程

5.3.5 內網穿透

5.3.5.0 為什么使用內網穿透?

支付寶需要回調我們的接口進行異步通知

5.3.5.1 簡介

內網穿透功能可以允許我們使用外網的網址來訪問主機;
正常的外網需要訪問我們項目的流程是:

  1. 買服務器并且有公網固定 IP
  2. 買域名映射到服務器的 IP
  3. 域名需要進行備案和審核
5.3.5.2 使用場景

1、 開發測試(微信、 支付寶)
2、 智慧互聯
3、 遠程控制
4、 私有云

5.3.5.3 內網穿透的幾個常用軟件

1、 natapp: https://natapp.cn/ 優惠碼: 022B93FD(9 折) [僅限第一次使用]
2、 續斷: www.zhexi.tech 優惠碼: SBQMEA(95 折) [僅限第一次使用]
3、 花生殼: https://www.oray.com/

5.3.5.4 natapp內網穿透

官方文檔NATAPP1分鐘快速新手圖文教程 - NATAPP-內網穿透 基于ngrok的國內高速內網映射工具

  1. 注冊,實名認證

  2. 購買免費隧道后拿到authtoken

  3. window啟動命令

    natapp -authtoken=你的authtoken
    

5.3.6 整合支付

5.3.6.1 導入依賴

gulimall-order/pom.xml

        <!-- 支付寶sdk --><!-- https://mvnrepository.com/artifact/com.alipay.sdk/alipay-sdk-java --><dependency><groupId>com.alipay.sdk</groupId><artifactId>alipay-sdk-java</artifactId><version>4.10.111.ALL</version></dependency>
5.3.6.2 抽取阿里云支付模板

gulimall-order/src/main/java/site/zhourui/gulimall/order/config/AlipayTemplate.java

package site.zhourui.gulimall.order.config;import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.request.AlipayTradePagePayRequest;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;/*** @author zr* @date 2021/12/31 10:26*/
@ConfigurationProperties(prefix = "alipay")
@Component
@Data
public class AlipayTemplate {// 應用ID,您的APPID,收款賬號既是您的APPID對應支付寶賬號public String app_id;// 商戶私鑰,您的PKCS8格式RSA2私鑰public String merchant_private_key;// 支付寶公鑰,查看地址:https://openhome.alipay.com/platform/keyManage.htm 對應APPID下的支付寶公鑰。public String alipay_public_key;// 服務器[異步通知]頁面路徑  需http://格式的完整路徑,不能加?id=123這類自定義參數,必須外網可以正常訪問// 支付寶會悄悄的給我們發送一個請求,告訴我們支付成功的信息public String notify_url;// 頁面跳轉同步通知頁面路徑 需http://格式的完整路徑,不能加?id=123這類自定義參數,必須外網可以正常訪問//同步通知,支付成功,一般跳轉到成功頁public String return_url;// 簽名方式private  String sign_type;// 字符編碼格式private  String charset;//訂單超時時間private String timeout = "1m";// 支付寶網關; https://openapi.alipaydev.com/gateway.dopublic String gatewayUrl;public  String pay(PayVo vo) throws AlipayApiException {//AlipayClient alipayClient = new DefaultAlipayClient(AlipayTemplate.gatewayUrl, AlipayTemplate.app_id, AlipayTemplate.merchant_private_key, "json", AlipayTemplate.charset, AlipayTemplate.alipay_public_key, AlipayTemplate.sign_type);//1、根據支付寶的配置生成一個支付客戶端AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl,app_id, merchant_private_key, "json",charset, alipay_public_key, sign_type);//2、創建一個支付請求 //設置請求參數AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();alipayRequest.setReturnUrl(return_url);alipayRequest.setNotifyUrl(notify_url);//商戶訂單號,商戶網站訂單系統中唯一訂單號,必填String out_trade_no = vo.getOut_trade_no();//付款金額,必填String total_amount = vo.getTotal_amount();//訂單名稱,必填String subject = vo.getSubject();//商品描述,可空String body = vo.getBody();alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","+ "\"total_amount\":\""+ total_amount +"\","+ "\"subject\":\""+ subject +"\","+ "\"body\":\""+ body +"\","+ "\"timeout_express\":\""+timeout+"\","+ "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");String result = alipayClient.pageExecute(alipayRequest).getBody();//會收到支付寶的響應,響應的是一個頁面,只要瀏覽器顯示這個頁面,就會自動來到支付寶的收銀臺頁面System.out.println("支付寶響應:登錄頁面的代碼\n"+result);return result;}@Datapublic static class PayVo {private String out_trade_no; // 商戶訂單號 必填private String subject; // 訂單名稱 必填private String total_amount;  // 付款金額 必填private String body; // 商品描述 可空}
}
5.3.6.3 配置模板所需相關配置
alipay:alipay_public_key: xxxapp_id: 2021000117672854charset: utf-8gatewayUrl: https://openapi.alipaydev.com/gateway.domerchant_private_key: xxxx#此處先使用demo的回調接口頁面notify_url: http://4wa8cx.natappfree.cc/alipay_trade_wap_pay_java_utf_8_Web_exploded/notify_url.jspreturn_url: http://4wa8cx.natappfree.cc/alipay_trade_wap_pay_java_utf_8_Web_exploded/return_url.jspsign_type: RSA2
5.3.6.4 支付寶支付接口

gulimall-order/src/main/java/site/zhourui/gulimall/order/web/PayWebController.java

package site.zhourui.gulimall.order.web;import com.alipay.api.AlipayApiException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import site.zhourui.gulimall.order.config.AlipayTemplate;
import site.zhourui.gulimall.order.service.OrderService;/*** @author zr* @date 2021/12/31 10:57*/
@Slf4j
@Controller
public class PayWebController {@Autowiredprivate AlipayTemplate alipayTemplate;@Autowiredprivate OrderService orderService;/*** 用戶下單:支付寶支付* 1、讓支付頁讓瀏覽器展示* 2、支付成功以后,跳轉到用戶的訂單列表頁* @param orderSn* @return* @throws AlipayApiException*/@ResponseBody@GetMapping(value = "/aliPayOrder",produces = "text/html")public String aliPayOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException {AlipayTemplate.PayVo payVo = orderService.getOrderPay(orderSn);// 支付寶返回一個頁面【支付寶賬戶登錄的html頁面】String pay = alipayTemplate.pay(payVo);System.out.println(pay);return pay;}
}

獲取當前訂單的支付信息

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/OrderService.java

    /*** 獲取當前訂單的支付信息* @param orderSn* @return*/AlipayTemplate.PayVo getOrderPay(String orderSn);

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

    /*** 獲取當前訂單的支付信息*/@Overridepublic AlipayTemplate.PayVo getOrderPay(String orderSn) {AlipayTemplate.PayVo payVo = new AlipayTemplate.PayVo();OrderEntity orderInfo = this.getOrderByOrderSn(orderSn);//保留兩位小數點,向上取值BigDecimal payAmount = orderInfo.getPayAmount().setScale(2, BigDecimal.ROUND_UP);payVo.setTotal_amount(payAmount.toString());payVo.setOut_trade_no(orderInfo.getOrderSn());//查詢訂單項的數據List<OrderItemEntity> orderItemInfo = orderItemService.list(new QueryWrapper<OrderItemEntity>().eq("order_sn", orderSn));OrderItemEntity orderItemEntity = orderItemInfo.get(0);payVo.setBody(orderItemEntity.getSkuAttrsVals());payVo.setSubject(orderItemEntity.getSkuName());return payVo;}
5.3.6.5 前端支付頁面

https://gitee.com/zhourui815/gulimall/blob/master/gulimall-order/src/main/resources/templates/pay.html

5.3.6.6 支付測試

支付成功后跳轉頁面,正常情況下應該跳轉到訂單列表頁面

5.3.6.7 訂單列表頁渲染
5.3.6.7.1 靜態資源上傳

5.3.6.7.2 配置host

5.3.6.7.3 配置網關

gulimall-gateway/src/main/resources/application.yml

- id: gulimall_member_routeuri: lb://gulimall-memberpredicates:- Host=member.gulimall.com
5.3.6.7.3 前端頁面

前端頁面https://gitee.com/zhourui815/gulimall/blob/master/gulimall-member/src/main/resources/templates/orderList.html

5.3.6.7.4 回調頁面接口

gulimall-member/src/main/java/site/zhourui/gulimall/member/web/MemberWebController.java

package site.zhourui.gulimall.member.web;import com.alibaba.fastjson.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import site.zhourui.common.utils.R;
import site.zhourui.gulimall.member.feign.OrderFeignService;import java.util.HashMap;
import java.util.Map;/*** @author zr* @date 2021/12/31 15:23*/
@Controller
public class MemberWebController {@Autowiredprivate OrderFeignService orderFeignService;@GetMapping(value = "/memberOrder.html")public String memberOrderPage(@RequestParam(value = "pageNum",required = false,defaultValue = "0") Integer pageNum,Model model) {//獲取到支付寶給我們轉來的所有請求數據//request,驗證簽名//查出當前登錄用戶的所有訂單列表數據Map<String,Object> page = new HashMap<>();page.put("page",pageNum.toString());//遠程查詢訂單服務訂單數據R orderInfo = orderFeignService.listWithItem(page);System.out.println(JSON.toJSONString(orderInfo));model.addAttribute("orders",orderInfo);return "orderList";}}

需要調用訂單遠程服務

gulimall-member/src/main/java/site/zhourui/gulimall/member/feign/OrderFeignService.java

package site.zhourui.gulimall.member.feign;import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import site.zhourui.common.utils.R;import java.util.Map;/*** @author zr* @date 2021/12/31 15:24*/
@FeignClient("gulimall-order")
public interface OrderFeignService {/*** 分頁查詢當前登錄用戶的所有訂單信息*/@PostMapping("/order/order/listWithItem")R listWithItem(@RequestBody Map<String, Object> params);}

gulimall-order/src/main/java/site/zhourui/gulimall/order/controller/OrderController.java

    /*** member遠程調用:分頁查詢當前登錄用戶的所有訂單信息*/@PostMapping("/listWithItem")//@RequiresPermissions("order:order:list")public R listWithItem(@RequestBody Map<String, Object> params){PageUtils page = orderService.queryPageWithItem(params);return R.ok().put("page", page);}

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/OrderService.java

    /*** 查詢當前用戶所有訂單數據* @param params* @return*/PageUtils queryPageWithItem(Map<String, Object> params);

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

此處需要設置訂單行信息

    /*** 查詢當前用戶所有訂單數據*/@Overridepublic PageUtils queryPageWithItem(Map<String, Object> params) {MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();IPage<OrderEntity> page = this.page(new Query<OrderEntity>().getPage(params),new QueryWrapper<OrderEntity>().eq("member_id",memberResponseVo.getId()).orderByDesc("create_time"));//遍歷所有訂單集合List<OrderEntity> orderEntityList = page.getRecords().stream().map(order -> {//根據訂單號查詢訂單項里的數據List<OrderItemEntity> orderItemEntities = orderItemService.list(new QueryWrapper<OrderItemEntity>().eq("order_sn", order.getOrderSn()));order.setOrderItemEntityList(orderItemEntities);return order;}).collect(Collectors.toList());page.setRecords(orderEntityList);return new PageUtils(page);}

為OrderEntity 新增屬性

	@TableField(exist = false)private List<OrderItemEntity> orderItemEntityList;
5.3.6.8 會員服務整合spring session(需要登錄后查看訂單信息)
5.3.6.8.1 配置攔截器

gulimall-member/src/main/java/site/zhourui/gulimall/member/interceptor/LoginUserInterceptor.java

新增攔截器,放行member/**,遠程調用接口

package site.zhourui.gulimall.member.interceptor;import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import site.zhourui.common.constant.AuthServerConstant;
import site.zhourui.common.vo.MemberResponseVo;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.PrintWriter;/*** @author zr* @date 2021/12/31 15:34*/
@Component
public class LoginUserInterceptor implements HandlerInterceptor {public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>();@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String uri = request.getRequestURI();boolean match = new AntPathMatcher().match("/member/**", uri);if (match) {return true;}HttpSession session = request.getSession();//獲取登錄的用戶信息MemberResponseVo attribute = (MemberResponseVo) session.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;}}
}

gulimall-member/src/main/java/site/zhourui/gulimall/member/config/MemberWebConfig.java

package site.zhourui.gulimall.member.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import site.zhourui.gulimall.member.interceptor.LoginUserInterceptor;/*** @author zr* @date 2021/12/31 15:36*/
@Configuration
public class MemberWebConfig implements WebMvcConfigurer {@Autowiredprivate LoginUserInterceptor loginUserInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");}}
5.3.6.8.2 整合springsession

依賴

        <dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId></dependency><!--redis--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><exclusions><exclusion><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId></exclusion></exclusions></dependency><!--jedis,redis客戶端--><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId></dependency>

配置文件

spring:application:name: gulimall-memberredis:port: 6379host: 192.168.157.128jackson:date-format: yyyy-MM-dd HH:mm:sssession:store-type: redis

session自定義配置

gulimall-member/src/main/java/site/zhourui/gulimall/member/config/GulimallSessionConfig.java

package site.zhourui.gulimall.member.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;/*** @author zr* @date 2021/12/12 10:29*/
@Configuration
public class GulimallSessionConfig {@Beanpublic CookieSerializer cookieSerializer() {DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();//放大作用域cookieSerializer.setDomainName("gulimall.com");cookieSerializer.setCookieName("GULISESSION");cookieSerializer.setCookieMaxAge(60*60*24*7);return cookieSerializer;}@Beanpublic RedisSerializer<Object> springSessionDefaultRedisSerializer() {return new GenericJackson2JsonRedisSerializer();}
}

解決feign遠程調用請求頭丟失問題

gulimall-member/src/main/java/site/zhourui/gulimall/member/config/GuliFeignConfig.java

package site.zhourui.gulimall.member.config;import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;/*** @author zr* @date 2021/12/31 15:36*/
@Configuration
public class GuliFeignConfig {@Bean("requestInterceptor")public RequestInterceptor requestInterceptor() {RequestInterceptor requestInterceptor = new RequestInterceptor() {@Overridepublic void apply(RequestTemplate template) {//1、使用RequestContextHolder拿到剛進來的請求數據ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (requestAttributes != null) {//老請求HttpServletRequest request = requestAttributes.getRequest();if (request != null) {//2、同步請求頭的數據(主要是cookie)//把老請求的cookie值放到新請求上來,進行一個同步String cookie = request.getHeader("Cookie");template.header("Cookie", cookie);}}}};return requestInterceptor;}
}
5.3.6.9 設置支付成功回調接口

修改return_url地址為member服務的訂單列表頁請求地址

gulimall-order/src/main/resources/application.yaml

  return_url: http://member.gulimall.com/memberOrder.html
5.3.6.10 獲取支付寶異步通知
  • 訂單支付成功后支付寶會回調商戶接口,這個時候需要修改訂單狀態
  • 由于同步跳轉可能由于網絡問題失敗,所以使用異步通知
  • 支付寶使用的是最大努力通知方案,保障數據一致性,隔一段時間會通知商戶支付成功,直到返回success
5.3.6.10.1 接收支付寶異步通知接口

gulimall-order/src/main/java/site/zhourui/gulimall/order/listener/OrderPayedListener.java

package site.zhourui.gulimall.order.listener;import com.alipay.api.AlipayApiException;
import com.alipay.api.internal.util.AlipaySignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import site.zhourui.gulimall.order.config.AlipayTemplate;
import site.zhourui.gulimall.order.service.OrderService;
import site.zhourui.gulimall.order.vo.PayAsyncVo;import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;/*** @author zr* @date 2021/12/31 17:30*/
@RestController
public class OrderPayedListener {@Autowiredprivate OrderService orderService;@Autowiredprivate AlipayTemplate alipayTemplate;@PostMapping(value = "/payed/notify")public String handleAlipayed(PayAsyncVo asyncVo, HttpServletRequest request) throws AlipayApiException, UnsupportedEncodingException {// 只要收到支付寶的異步通知,返回 success 支付寶便不再通知// 獲取支付寶POST過來反饋信息//TODO 需要驗簽Map<String, String> params = new HashMap<>();Map<String, String[]> requestParams = request.getParameterMap();for (String name : requestParams.keySet()) {String[] values = requestParams.get(name);String valueStr = "";for (int i = 0; i < values.length; i++) {valueStr = (i == values.length - 1) ? valueStr + values[i]: valueStr + values[i] + ",";}//亂碼解決,這段代碼在出現亂碼時使用// valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");params.put(name, valueStr);}boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayTemplate.getAlipay_public_key(),alipayTemplate.getCharset(), alipayTemplate.getSign_type()); //調用SDK驗證簽名if (signVerified) {System.out.println("簽名驗證成功...");//去修改訂單狀態String result = orderService.handlePayResult(asyncVo);return result;} else {System.out.println("簽名驗證失敗...");return "error";}}}

處理支付寶支付結果

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

    /*** 處理支付寶的支付結果**/@Transactional(rollbackFor = Exception.class)@Overridepublic String handlePayResult(PayAsyncVo asyncVo) {//保存交易流水信息PaymentInfoEntity paymentInfo = new PaymentInfoEntity();paymentInfo.setOrderSn(asyncVo.getOut_trade_no());paymentInfo.setAlipayTradeNo(asyncVo.getTrade_no());paymentInfo.setTotalAmount(new BigDecimal(asyncVo.getBuyer_pay_amount()));paymentInfo.setSubject(asyncVo.getBody());paymentInfo.setPaymentStatus(asyncVo.getTrade_status());paymentInfo.setCreateTime(new Date());paymentInfo.setCallbackTime(asyncVo.getNotify_time());//添加到數據庫中this.paymentInfoService.save(paymentInfo);//修改訂單狀態//獲取當前狀態String tradeStatus = asyncVo.getTrade_status();if (tradeStatus.equals("TRADE_SUCCESS") || tradeStatus.equals("TRADE_FINISHED")) {//支付成功狀態String orderSn = asyncVo.getOut_trade_no(); //獲取訂單號this.updateOrderStatus(orderSn,OrderStatusEnum.PAYED.getCode(), PayConstant.ALIPAY);}return "success";}/*** 修改訂單狀態* @param orderSn* @param code*/private void updateOrderStatus(String orderSn, Integer code,Integer payType) {this.baseMapper.updateOrderStatus(orderSn,code,payType);}

修改訂單狀態

gulimall-order/src/main/java/site/zhourui/gulimall/order/dao/OrderDao.java

    /*** 修改訂單狀態* @param orderSn* @param code* @param payType*/void updateOrderStatus(@Param("orderSn") String orderSn,@Param("code") Integer code,@Param("payType") Integer payType);

gulimall-order/src/main/resources/mapper/order/OrderDao.xml

    <update id="updateOrderStatus">UPDATE oms_orderSET `status` = #{code},modify_time = NOW(),pay_type = #{payType},payment_time = NOW()WHERE order_sn = #{orderSn}</update>
5.3.6.11 設置支付寶異步通知接口地址

修改notify_url地址為訂單服務的回調接口地址

gulimall-order/src/main/resources/application.yaml

  notify_url: http://4wa8cx.natappfree.cc/payed/notify
5.3.6.12 是異步通知接口不被攔截

gulimall-order/src/main/java/site/zhourui/gulimall/order/interceptor/LoginUserInterceptor.java

package site.zhourui.gulimall.order.interceptor;/*** @author zr* @date 2021/12/21 22:04*/import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import site.zhourui.common.constant.AuthServerConstant;
import site.zhourui.common.vo.MemberResponseVo;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.PrintWriter;import static site.zhourui.common.constant.AuthServerConstant.LOGIN_USER;/*** 登錄攔截器* 從session中獲取了登錄信息(redis中),封裝到了ThreadLocal中*/
@Component
public class LoginUserInterceptor implements HandlerInterceptor {public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>();@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {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;}HttpSession session = request.getSession();MemberResponseVo memberResponseVo = (MemberResponseVo) session.getAttribute(AuthServerConstant.LOGIN_USER);if (memberResponseVo != null) {loginUser.set(memberResponseVo);return true;}else {session.setAttribute("msg","請先登錄");response.sendRedirect("http://auth.gulimall.com/login.html");return false;}}
}
5.3.6.13 內網穿透設置異步通知地址
  • 將外網映射到本地的order.gulimall.com:80
  • 由于回調的請求頭不是order.gulimall.com,因此nginx轉發到網關后找不到對應的服務,所以需要對nginx進行設置

修改內網穿透接口

測試

nginx 配置域名轉發

    listen       80;server_name   gulimall.com *.gulimall.com *.natappfree.cc;#server_name  search.gulimall.com;#charset koi8-r;#access_log  /var/log/nginx/log/host.access.log  main;location /static/ {root  /usr/share/nginx/html;}location /payed/ {proxy_set_header Host order.gulimall.com;proxy_pass http://gulimall;}location / {proxy_set_header Host $host;proxy_pass http://gulimall;}

攔截器放行通知接口

gulimall-order/src/main/java/site/zhourui/gulimall/order/interceptor/LoginUserInterceptor.java

    @Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {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;}HttpSession session = request.getSession();MemberResponseVo memberResponseVo = (MemberResponseVo) session.getAttribute(AuthServerConstant.LOGIN_USER);if (memberResponseVo != null) {loginUser.set(memberResponseVo);return true;}else {session.setAttribute("msg","請先登錄");response.sendRedirect("http://auth.gulimall.com/login.html");return false;}}
5.3.6.13.1 日期格式問題
Field error in object 'payAsyncVo' on field 'notify_time': rejected value [2022-01-02 10:50:06]; codes [typeMismatch.payAsyncVo.notify_time,typeMismatch.notify_time,typeMismatch.java.util.Date,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [payAsyncVo.notify_time,notify_time]; arguments []; default message [notify_time]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.util.Date' for property 'notify_time'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.util.Date] for value '2022-01-02 10:50:06'; nested exception is java.lang.IllegalArgumentException]]

解決方案

spring:mvc:date-format: yyyy-MM-dd HH:mm:ss

訂單號長度報錯

修改oms_payment_info 的訂單號長度

5.3.6.9 支付測試

付款成功后自動跳轉到訂單列表頁

5.3.6.10 收單
  1. 訂單在支付頁,不支付,一直刷新,訂單過期了才支付,訂單狀態改為已支付了,但是庫存解鎖了。

    • 使用支付寶自動收單功能解決。只要一段時間不支付,就不能支付了。

    • 效果

  2. 由于時延等問題。訂單解鎖完成,正在解鎖庫存的時候,異步通知才到

    • 訂單解鎖,手動調用收單
  3. 網絡阻塞問題,訂單支付成功的異步通知一直不到達

    • 查詢訂單列表時,ajax獲取當前未支付的訂單狀態,查詢訂單狀態時,再獲取一下支付寶此訂單的狀態
  4. 其他各種問題

    • 每天晚上閑時下載支付寶對賬單,一一進行對賬

6 接口冪等性

6.1 什么是冪等性

接口冪等性就是用戶對于同一操作發起的一次請求或者多次請求的結果是一致的, 不會因為多次點擊而產生了副作用; 比如說支付場景, 用戶購買了商品支付扣款成功, 但是返回結果的時候網絡異常, 此時錢已經扣了, 用戶再次點擊按鈕, 此時會進行第二次扣款, 返回結果成功, 用戶查詢余額返發現多扣錢了, 流水記錄也變成了兩條. . . ,這就沒有保證接口的冪等性。

6.2 哪些情況需要防止

  • 用戶多次點擊按鈕
  • 用戶頁面回退再次提交
  • 微服務互相調用, 由于網絡問題, 導致請求失敗。 feign 觸發重試機制
  • 其他業務情況

6.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 不是主鍵, 可以重復, 那上面業務多次操作, 數據都會新增多條, 不具備冪等性。

6.4 冪等解決方案

6.4.1 token 機制 (本次使用)

  1. 服務端提供了發送 token 的接口。 我們在分析業務的時候, 哪些業務是存在冪等問題的,就必須在執行業務前, 先去獲取 token, 服務器會把 token 保存到 redis 中。
  2. 然后調用業務接口請求時, 把 token 攜帶過去, 一般放在請求頭部。
  3. 服務器判斷 token 是否存在 redis 中, 存在表示第一次請求, 然后刪除 token,繼續執行業務。
  4. 如果判斷 token 不存在 redis 中, 就表示是重復操作, 直接返回重復標記給 client, 這樣就保證了業務代碼, 不被重復執行。

危險性:

  1. 刪除 token 還是后刪除 token;

    • 先刪除可能導致, 業務確實沒有執行, 重試還帶上之前 token, 由于防重設計導致,請求還是不能執行。
    • 后刪除可能導致, 業務處理成功, 但是服務閃斷, 出現超時, 沒有刪除 token, 別人繼續重試, 導致業務被執行兩邊
    • 我們最好設計為先刪除 token, 如果業務調用失敗, 就重新獲取 token 再次請求。
  2. Token 獲取、 比較和刪除必須是原子性

    • redis.get(token) 、 token.equals、 redis.del(token)如果這兩個操作不是原子, 可能導致, 高并發下, 都 get 到同樣的數據, 判斷都成功, 繼續業務并發執行
    • 可以在 redis 使用 lua 腳本完成這個操作
    if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end  
    

6.4.2 各種鎖機制

6.4.2.1 數據庫悲觀鎖

select * from xxxx where id = 1 for update;
悲觀鎖使用時一般伴隨事務一起使用, 數據鎖定時間可能會很長, 需要根據實際情況選用。另外要注意的是, id 字段一定是主鍵或者唯一索引, 不然可能造成鎖表的結果, 處理起來會非常麻煩。

6.4.2.2 數據庫樂觀鎖

這種方法適合在更新的場景中,
update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1根據 version 版本, 也就是在操作庫存前先獲取當前商品的 version 版本號, 然后操作的時候帶上此 version 號。 我們梳理下, 我們第一次操作庫存時, 得到 version 為 1, 調用庫存服務version 變成了 2; 但返回給訂單服務出現了問題, 訂單服務又一次發起調用庫存服務, 當訂單服務傳如的 version 還是 1, 再執行上面的 sql 語句時, 就不會執行; 因為 version 已經變為 2 了, where 條件就不成立。 這樣就保證了不管調用幾次, 只會真正的處理一次。樂觀鎖主要使用于處理讀多寫少的問題

6.4.2.3 業務層分布式鎖

如果多個機器可能在同一時間同時處理相同的數據, 比如多臺機器定時任務都拿到了相同數據處理, 我們就可以加分布式鎖, 鎖定此數據, 處理完成后釋放鎖。 獲取到鎖的必須先判斷這個數據是否被處理過。

6.4.3 各種唯一約束

6.4.3.1 數據庫唯一約束

插入數據, 應該按照唯一索引進行插入, 比如訂單號, 相同的訂單就不可能有兩條記錄插入。我們在數據庫層面防止重復。

這個機制是利用了數據庫的主鍵唯一約束的特性, 解決了在 insert 場景時冪等問題。 但主鍵的要求不是自增的主鍵, 這樣就需要業務生成全局唯一的主鍵。

如果是分庫分表場景下, 路由規則要保證相同請求下, 落地在同一個數據庫和同一表中, 要不然數據庫主鍵約束就不起效果了, 因為是不同的數據庫和表主鍵不相關。

6.4.3.2 redis set 防重

很多數據需要處理, 只能被處理一次, 比如我們可以計算數據的 MD5 將其放入 redis 的 set,每次處理數據, 先看這個 MD5 是否已經存在, 存在就不處理。

6.4.4 防重表

使用訂單號 orderNo 做為去重表的唯一索引, 把唯一索引插入去重表, 再進行業務操作, 且他們在同一個事務中。 這個保證了重復請求時, 因為去重表有唯一約束, 導致請求失敗, 避免了冪等問題。 這里要注意的是, 去重表和業務表應該在同一庫中, 這樣就保證了在同一個事務, 即使業務操作失敗了, 也會把去重表的數據回滾。 這個很好的保證了數據一致性。之前說的 redis 防重也算

6.4.5 全局請求唯一 id

調用接口時, 生成一個唯一 id, redis 將數據保存到集合中(去重) , 存在即處理過??梢允褂?nginx 設置每一個請求的唯一 id;

proxy_set_header X-Request-Id $request_id;  

7 本地事務與分布式事務

7.1 本地事務

7.1.1 事務的基本性質

數據庫事務的幾個特性: 原子性(Atomicity )、 一致性( Consistency )、 隔離性或獨立性( Isolation)和持久性(Durabilily), 簡稱就是 ACID;

  • 原子性: 一系列的操作整體不可拆分, 要么同時成功, 要么同時失敗
  • 一致性: 數據在事務的前后, 業務整體一致。
    • 轉賬。 A:1000; B:1000; 轉 200 事務成功; A: 800 B: 1200
  • 隔離性: 事務之間互相隔離。
  • 持久性: 一旦事務成功, 數據一定會落盤在數據庫。

在以往的單體應用中, 我們多個業務操作使用同一條連接操作不同的數據表, 一旦有異常,我們可以很容易的整體回滾;

7.1.2 事務的隔離級別

  1. READ UNCOMMITTED(讀未提交)
    該隔離級別的事務會讀到其它未提交事務的數據, 此現象也稱之為臟讀。
  2. READ COMMITTED( 讀提交)
    一個事務可以讀取另一個已提交的事務, 多次讀取會造成不一樣的結果, 此現象稱為不可重復讀問題, Oracle 和 SQL Server 的默認隔離級別。
  3. REPEATABLE READ( 可重復讀)
    該隔離級別是 MySQL 默認的隔離級別, 在同一個事務里, select 的結果是事務開始時時間點的狀態, 因此, 同樣的 select 操作讀到的結果會是一致的, 但是, 會有幻讀現象。 MySQL的 InnoDB 引擎可以通過 next-key locks 機制( 參考下文"行鎖的算法"一節) 來避免幻讀。
  4. SERIALIZABLE( 序列化)
    在該隔離級別下事務都是串行順序執行的, MySQL 數據庫的 InnoDB 引擎會給讀操作隱式加一把讀共享鎖, 從而避免了臟讀、 不可重讀復讀和幻讀問題。

7.1.3 事務的傳播行為

  1. PROPAGATION_REQUIRED: 如果當前沒有事務, 就創建一個新事務, 如果當前存在事務,就加入該事務, 該設置是最常用的設置。
  2. PROPAGATION_SUPPORTS: 支持當前事務, 如果當前存在事務, 就加入該事務, 如果當前不存在事務, 就以非事務執行。
  3. PROPAGATION_MANDATORY: 支持當前事務, 如果當前存在事務, 就加入該事務, 如果當前不存在事務, 就拋出異常。
  4. PROPAGATION_REQUIRES_NEW: 創建新事務, 無論當前存不存在事務, 都創建新事務。
  5. PROPAGATION_NOT_SUPPORTED: 以非事務方式執行操作, 如果當前存在事務, 就把當前事務掛起。
  6. PROPAGATION_NEVER: 以非事務方式執行, 如果當前存在事務, 則拋出異常。
  7. PROPAGATION_NESTED: 如果當前存在事務, 則在嵌套事務內執行。 如果當前沒有事務,則執行與 PROPAGATION_REQUIRED 類似的操作。

7.1.4 SpringBoot 事務關鍵點

7.1.4.1 事務的自動配置

TransactionAutoConfiguration

7.1.4.2 本地事務的坑

在同一個類里面, 編寫兩個方法, 內部調用的時候, 會導致事務設置失效。 原因是沒有用到代理對象的緣故。

解決辦法

  1. 導入 spring-boot-starter-aop
  2. @EnableTransactionManagement(proxyTargetClass = true)
  3. @EnableAspectJAutoProxy(exposeProxy=true)
  4. AopContext.currentProxy() 調用方法

示例:

1、如果方法a、b、c都在同一個service里面,事務傳播行為不生效,共享一個事務原理:事務是用代理對象來控制的,內部調用b(),c(),就相當于直接調用沒有經過事務【繞過了代理對象】解決:不能使用this.b();也不能注入自己【要使用代理對象來調用事務方法】@Transactional(timeout=30)
public void a() {b();// a事務傳播給了b事務,并且b事務的設置失效c();// c單獨創建一個新事務
}@Transactional(propagation = Propagation.REQUIRED, timeout=2)
public void b() {}@Transactional(propagation = Propagation.REQUIRES_NEW)
public void c() {}

解決步驟

具體步驟:
1、引入aop依賴<!-- 引入aop,解決本地事務失效問題 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>2、開啟動態代理【默認使用jdk動態代理,需要有接口】
@EnableAspectJAutoProxy(exposeProxy = true)     //開啟了aspect動態代理模式,對外暴露代理對象
好處:cglib繼承的方式完成動態代理
exposeProxy = true:對外暴露代理對象3、獲取動態代理對象
OrderServiceImpl orderService = (OrderServiceImpl)AopContext.currentProxy();
orderService.b();
orderService.c();

7.2 分布式事務

7.2.1 為什么會有分布式事務?

分布式系統經常出現的異常機器宕機、 網絡異常、 消息丟失、 消息亂序、 數據錯誤、 不可靠的 TCP、 存儲數據丟失…

分布式事務是企業集成中的一個技術難點, 也是每一個分布式系統架構中都會涉及到的一個東西, 特別是在微服務架構中, 幾乎可

以說是無法避免。

7.2.2 CAP定理與base理論

7.2.2.1 CAP定理

CAP 原則又稱 CAP 定理, 指的是在一個分布式系統中

  • 一致性(Consistency) :
    • 在分布式系統中的所有數據備份, 在同一時刻是否同樣的值。 (等同于所有節點訪問同一份最新的數據副本)
  • 可用性(Availability)
    • 在集群中一部分節點故障后, 集群整體是否還能響應客戶端的讀寫請求。 (對數據更新具備高可用性)
  • 分區容錯性(Partition tolerance)
    • 大多數分布式系統都分布在多個子網絡。 每個子網絡就叫做一個區(partition) 。分區容錯的意思是, 區間通信可能失敗。 比如, 一臺服務器放在中國, 另一臺服務器放在美國, 這就是兩個區, 它們之間可能無法通信。

CAP 原則指的是, 這三個要素最多只能同時實現兩點, 不可能三者兼顧

一般來說, 分區容錯無法避免, 因此可以認為 CAP 的 P 總是成立。 CAP 定理告訴我們,剩下的 C 和 A 無法同時做到(CA沒有P就是單體應用,沒有必要)。

如果滿足P,此時要滿足A(所有機器都可用包括通信故障那臺【數據未同步】),就不能保證一致性【同步數據的通信線故障,無法同步】
如果滿足P,此時要滿足C,那網絡通信故障的節點就不應該繼續提供服務(因為他的數據不一致)【宕機的那臺機器數據 無法同步】
AP:容易,就算未同步的數據也可用
CP:犧牲可用性

? 1、算法:raft和paxos算法:http://thesecretlivesofdata.com/raft/【raft算法演示】

7.2.2.2 raft算法
7.2.2.2.1 領導選舉機制

  1. 集群所有節點啟動默認都是隨從狀態,在此期間每個隨從都會自旋,如果該節點滿足了自旋時間,那么該節點就會成為候選者,領導者需要在一定的時間內為所有隨從者響應,告知自己還活著(心跳),隨從者收到響應就會終止本次自旋開始新一輪的自旋
  2. 如果沒有監聽到到領導者的心跳,滿足自旋時間的節點變成候選者,同時向其他節點發送投票請求(同時終止其自旋),終止其他節點自旋,成為候選者的可能有多個,此時候選者發起投票,直到有一個候選者獲勝
  3. 最終成為領導

具體步驟:
1、選舉超時 election timeout
隨從變成候選者的時間【150ms and 300ms隨機的】【自旋時間,如果沒有收到領導的命令變成候選者】
例如:啟動集群,3個節點獲得隨機自旋時間,自旋時間到了就成為候選節點
2、成為候選節點,并給自己投票1,然后給其他隨從節點發送選舉請求【隨從節點的票可能投給更快的候選者】
隨從節點的票一旦投出便重新自旋

3、心跳時間(heartbeat timeout):每隔一段時間發送一個心跳,然后隨從節點刷新自旋時間【小于300ms,否則大家都成為候選者了】
此時領導網絡延時,自旋結束產生候選者,產生新領導

4、有多個候選者,并且票數一樣,就自旋重新投

7.2.2.2.2 領導日志復制(可保證數據一致性)

所有節點修改數據,都要通過領導來修改

  1. 客戶端通知領導修改一個數據,領導先創建一個 節點日志
  2. 領導將這條日志 發送給所有所有隨從節點【隨從節點收到并返回確認消息給領導】
  3. 3、領導等待大多數隨從節點的確認消息,領導提交數據,然后通知隨從節點可以提交了
  4. 隨從節點也提交數據。最后領導節點給請求返回提交成功

具體步驟:
1、領導收到后并不會馬上給隨從節點發送 日志,等待下一次心跳時發送日志
2、然后領導提交并馬上返回請求提交成功。然后跟隨下一個心跳發送隨從 告訴其提交
3、可保證數據一致性【例如選出來兩個領導,不同機房。2個和3個組成兩個群】

demo:此時2個的那個客戶端發請求,一直保存失敗,因為不是大多數人成功【所以數據未提交】,但是另外一邊3個節點組成的集群可以保存成功【大多數節點】
如果此時兩個集群恢復了數據通信,舊領導退位,并且跟著舊領導未提交的數據需要回滾【低輪領導退位,新領導上位】
然后匹配上新領導的日志

7.2.2.3 CP的缺點

對于多數大型互聯網應用的場景, 主機眾多、 部署分散, 而且現在的集群規模越來越大, 所以節點故障、 網絡故障是常態, 而且要保證服務可用性達到 99.99999%(N 個 9) , 即保證P 和 A, 舍棄 C。

7.2.2.4 BASE 理論

是對 CAP 理論的延伸, 思想是即使無法做到強一致性(CAP 的一致性就是強一致性) , 但可以采用適當的采取弱一致性, 即最終一致性。

BASE 是指

  • 基本可用(Basically Available)
    • 基本可用是指分布式系統在出現故障的時候, 允許損失部分可用性(例如響應時間、功能上的可用性) , 允許損失部分可用性。 需要注意的是, 基本可用絕不等價于系統不可用
    • 響應時間上的損失: 正常情況下搜索引擎需要在 0.5 秒之內返回給用戶相應的查詢結果, 但由于出現故障(比如系統部分機房發生斷電或斷網故障) , 查詢結果的響應時間增加到了 1~2 秒。
    • 功能上的損失: 購物網站在購物高峰(如雙十一) 時, 為了保護系統的穩定性,部分消費者可能會被引導到一個降級頁面。
  • 軟狀態( Soft State)
    • 軟狀態是指允許系統存在中間狀態, 而該中間狀態不會影響系統整體可用性。 分布式存儲中一般一份數據會有多個副本, 允許不同副本同步的延時就是軟狀態的體現。 mysql replication 的異步復制也是一種體現。
  • 最終一致性( Eventual Consistency)
    • 最終一致性是指系統中的所有數據副本經過一定時間后, 最終能夠達到一致的狀態。 弱一致性和強一致性相反, 最終一致性是弱一致性的一種特殊情況。
7.2.2.5 強一致性、 弱一致性、 最終一致性

從客戶端角度, 多進程并發訪問時, 更新過的數據在不同進程如何獲取的不同策略, 決定了不同的一致性。 對于關系型數據庫, 要求更新過的數據能被后續的訪問都能看到, 這是強一致性。 如果能容忍后續的部分或者全部訪問不到, 則是弱一致性。 如果經過一段時間后要求能訪問到更新后的數據, 則是最終一致性

7.2.3 分布式事務幾種方案

7.2.3.1 2PC 模式

數據庫支持的 2PC【 2 phase commit 二階提交】 , 又叫做 XA Transactions。MySQL 從 5.5 版本開始支持, SQL Server 2005 開始

支持, Oracle 7 開始支持。其中, XA 是一個兩階段提交協議, 該協議分為以下兩個階段:

第一階段: 事務協調器要求每個涉及到事務的數據庫預提交(precommit)此操作, 并反映是否可以提交.

第二階段: 事務協調器要求每個數據庫提交數據。

其中, 如果有任何一個數據庫否決此次提交, 那么所有數據庫都會被要求回滾它們在此事務中的那部分信息。

  • XA 協議比較簡單, 而且一旦商業數據庫實現了 XA 協議, 使用分布式事務的成本也比較低。
  • XA 性能不理想, 特別是在交易下單鏈路, 往往并發量很高, XA 無法滿足高并發場景
  • XA 目前在商業數據庫支持的比較理想, 在 mysql 數據庫中支持的不太理想, mysql 的XA 實現, 沒有記錄 prepare 階段日志, 主備切換回導致主庫與備庫數據不一致。
  • 許多 nosql 也沒有支持 XA, 這讓 XA 的應用場景變得非常狹隘。
  • 也有 3PC, 引入了超時機制( 無論協調者還是參與者, 在向對方發送請求后, 若長時間未收到回應則做出相應處理)
7.2.3.2 柔性事務-TCC 事務補償型方案 (seata)

剛性事務: 遵循 ACID 原則, 強一致性。
柔性事務: 遵循 BASE 理論, 最終一致性;
與剛性事務不同, 柔性事務允許一定時間內, 不同節點的數據不一致, 但要求最終一致。

一階段 prepare 行為: 調用 自定義 的 prepare 邏輯。
二階段 commit 行為: 調用 自定義 的 commit 邏輯。
二階段 rollback 行為: 調用 自定義 的 rollback 邏輯。
所謂 TCC 模式, 是指支持把 自定義 的分支事務納入到全局事務的管理中(seata)。

實現:
將業務代碼拆成三部分。
1、try鎖庫存
2、confirm提交數據
3、事務補償邏輯:一旦出現異常執行cancel來回滾【取消鎖定庫存】

其實就是2PC的手動實現

7.2.3.3 柔性事務-最大努力通知型方案【支付寶支付】【多,高并發場景】【基于消息服務mq】

按規律進行通知, 不保證數據一定能通知成功, 但會提供可查詢操作接口進行核對。 這種方案主要用在與第三方系統通訊時, 比如: 調用微信或支付寶支付后的支付結果通知。 這種方案也是結合 MQ 進行實現, 例如: 通過 MQ 發送 http 請求, 設置最大通知次數。 達到通知次數后即不再通知。

案例: 銀行通知、 商戶通知等( 各大交易業務平臺間的商戶通知: 多次通知、 查詢校對、 對賬文件) , 支付寶的支付成功異步回調

例如支付寶支付成功,往MQ發送消息【隔幾秒發一個】
訂單訂閱topic,一旦訂單確認消息,給支付寶發送確認,支付寶就不再通知了

總結

以上是生活随笔為你收集整理的谷粒商城--订单服务--高级篇笔记十一的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。