文章目錄
- 0.學習目标
- 1.搭建購物車服務
-
- 1.1.建立module
- 1.2.pom依賴
- 1.3.配置檔案
- 1.4.啟動類
- 2.購物車功能分析
-
- 2.1.需求
- 2.2.流程圖
- 3.未登入購物車
-
- 3.1.準備
-
- 3.1.1購物車的資料結構
- 3.1.2.web本地存儲
-
- 什麼是web本地存儲?
- LocalStorage的用法
- 3.1.3.擷取num
- 3.2.添加購物車
-
- 3.2.1.點選事件
- 3.2.2.擷取數量,添加購物車
- 3.3.查詢購物車
-
- 3.3.1.校驗使用者登入
- 3.3.2.查詢購物車
- 3.5.2.渲染到頁面
- 3.6.修改數量
- 3.7.删除商品
- 3.8.選中商品
-
- 3.8.1.選中一個
- 3.8.2.初始化全選
- 3.8.3.總價格
- 4.已登入購物車
-
- 4.1.添加登入校驗
-
- 4.1.1.引入JWT相關依賴
- 4.1.2.配置公鑰
- 4.1.3.加載公鑰
- 4.1.4.編寫攔截器
- 4.1.5.配置攔截器
- 4.2.背景購物車設計
- 4.3.添加商品到購物車
-
- 4.3.1.頁面發起請求
- 4.3.2.編寫controller
- 4.3.3.CartService
- 4.3.4.GoodClient
- 4.3.5.結果
- 4.4.查詢購物車
-
- 4.4.1.頁面發起請求
- 4.4.2.背景實作
- 4.4.3.測試
- 4.5.修改商品數量
-
- 4.5.1.頁面發起請求
- 4.5.2.背景實作
- 4.6.删除購物車商品
-
- 4.6.1.頁面發起請求
- 4.6.2.背景實作
- 5.登入後購物車合并(作業)
-
- 5.1 修改前台
- 5.2 修改背景
0.學習目标
1.實作未登入狀态的購物車
2.實作登陸狀态下的購物車
源碼及筆記:
連結:https://pan.baidu.com/s/166zDGT7MYiPCh3o70tjSkA
提取碼:zau1
1.搭建購物車服務
1.1.建立module

1.2.pom依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>leyou</artifactId>
<groupId>com.leyou.parent</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.leyou.service</groupId>
<artifactId>ly-cart</artifactId>
<version>1.0.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
</project>
1.3.配置檔案
server:
port: 8088
spring:
application:
name: cart-service
redis:
host: 192.168.56.101
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
registry-fetch-interval-seconds: 10
instance:
lease-renewal-interval-in-seconds: 5
lease-expiration-duration-in-seconds: 15
1.4.啟動類
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class LeyouCartApplication {
public static void main(String[] args) {
SpringApplication.run(LeyouCartApplication.class, args);
}
}
2.購物車功能分析
2.1.需求
需求描述:
- 使用者可以在登入狀态下将商品添加到購物車
- 放入資料庫
- mongodb
- 放入redis(采用)
- 使用者可以在未登入狀态下将商品添加到購物車
- 放入localstorage
- cookie
- webSQL
- 使用者可以使用購物車一起結算下單
- 使用者可以查詢自己的購物車
- 使用者可以在購物車中修改購買商品的數量。
- 使用者可以在購物車中删除商品。
- 在購物車中展示商品優惠資訊
- 提示購物車商品價格變化
2.2.流程圖
這幅圖主要描述了兩個功能:新增商品到購物車、查詢購物車。
新增商品:
- 判斷是否登入
- 是:則添加商品到背景Redis中
- 否:則添加商品到本地的Localstorage
無論哪種新增,完成後都需要查詢購物車清單:
- 判斷是否登入
- 否:直接查詢localstorage中資料并展示
- 是:已登入,則需要先看本地是否有資料,
- 有:需要送出到背景添加到redis,合并資料,而後查詢
- 否:直接去背景查詢redis,而後傳回
3.未登入購物車
3.1.準備
3.1.1購物車的資料結構
首先分析一下未登入購物車的資料結構。
我們看下頁面展示需要什麼資料:
是以每一個購物車資訊,都是一個對象,包含:
{
skuId:2131241,
title:"小米6",
image:"",
price:190000,
num:1,
ownSpec:"{"機身顔色":"陶瓷黑尊享版","記憶體":"6GB","機身存儲":"128GB"}"
}
另外,購物車中不止一條資料,是以最終會是對象的數組。即:
[
{...},{...},{...}
]
3.1.2.web本地存儲
知道了資料結構,下一個問題,就是如何儲存購物車資料。前面我們分析過,可以使用Localstorage來實作。Localstorage是web本地存儲的一種,那麼,什麼是web本地存儲呢?
什麼是web本地存儲?
web本地存儲主要有兩種方式:
- LocalStorage:localStorage 方法存儲的資料沒有時間限制。第二天、第二周或下一年之後,資料依然可用。
- SessionStorage:sessionStorage 方法針對一個 session 進行資料存儲。當使用者關閉浏覽器視窗後,資料會被删除。
LocalStorage的用法
文法非常簡單:
localStorage.setItem("key","value"); // 存儲資料
localStorage.getItem("key"); // 擷取資料
localStorage.removeItem("key"); // 删除資料
注意:localStorage和SessionStorage都隻能儲存字元串。
不過,在我們的common.js中,已經對localStorage進行了簡單的封裝:
示例:
3.1.3.擷取num
添加購物車需要知道購物的數量,是以我們需要擷取數量大小。我們在Vue中定義num,儲存數量:
然後将num與頁面的input框綁定,同時給
+
和
-
的按鈕綁定事件:
編寫方法:
3.2.添加購物車
3.2.1.點選事件
我們看下商品詳情頁:
現在點選加入購物車會跳轉到購物車成功頁面。
不過我們不這麼做,我們綁定點選事件,然後實作添加購物車功能。
addCart方法中判斷使用者的登入狀态:
addCart(){
ly.http.get("/auth/verify").then(res=>{
// 已登入發送資訊到背景,儲存到redis中
}).catch(()=>{
// 未登入儲存在浏覽器本地的localStorage中
})
}
3.2.2.擷取數量,添加購物車
addCart(){
ly.verifyUser().then(res=>{
// 已登入發送資訊到背景,儲存到redis中
}).catch(()=>{
// 未登入儲存在浏覽器本地的localStorage中
// 1、查詢本地購物車
let carts = ly.store.get("carts") || [];
let cart = carts.find(c=>c.skuId===this.sku.id);
// 2、判斷是否存在
if (cart) {
// 3、存在更新數量
cart.num += this.num;
} else {
// 4、不存在,新增
cart = {
skuId: this.sku.id,
title: this.sku.title,
price: this.sku.price,
image: this.sku.images,
num: this.num,
ownSpec: this.ownSpec
}
carts.push(cart);
}
// 把carts寫回localstorage
ly.store.set("carts", carts);
// 跳轉
window.location.href = "http://www.leyou.com/cart.html";
});
}
結果:
添加完成後,頁面會跳轉到購物車結算頁面:cart.html
3.3.查詢購物車
3.3.1.校驗使用者登入
因為會多次校驗使用者登入狀态,是以我們封裝一個校驗的方法:
在common.js中:
在頁面item.html中使用該方法:
3.3.2.查詢購物車
頁面加載時,就應該去查詢購物車。
var cartVm = new Vue({
el: "#cartApp",
data: {
ly,
carts: [],// 購物車資料
},
created() {
this.loadCarts();
},
methods: {
loadCarts() {
// 先判斷登入狀态
ly.verifyUser().then(() => {
// 已登入
}).catch(() => {
// 未登入
this.carts = ly.store.get("carts") || [];
this.selected = this.carts;
})
}
}
components: {
shortcut: () => import("/js/pages/shortcut.js")
}
})
重新整理頁面,檢視控制台Vue執行個體:
3.5.2.渲染到頁面
接下來,我們在頁面中展示carts的資料:
要注意,價格的展示需要進行格式化,這裡使用的是我們在common.js中定義的formatPrice方法
效果:
3.6.修改數量
我們給頁面的
+
和
-
綁定點選事件,修改num 的值:
兩個事件:
increment(c) {
c.num++;
ly.verifyUser().then(() => {
// TODO 已登入,向背景發起請求
}).catch(() => {
// 未登入,直接操作本地資料
ly.store.set("carts", this.carts);
})
},
decrement(c) {
if (c.num <= 1) {
return;
}
c.num--;
ly.verifyUser().then(() => {
// TODO 已登入,向背景發起請求
}).catch(() => {
// 未登入,直接操作本地資料
ly.store.set("carts", this.carts);
})
}
3.7.删除商品
給删除按鈕綁定事件:
點選事件中删除商品:
deleteCart(i){
ly.verifyUser().then(res=>{
// TODO,已登入購物車
}).catch(()=>{
// 未登入購物車
this.carts.splice(i, 1);
ly.store.set("carts", this.carts);
})
}
3.8.選中商品
在頁面中,每個購物車商品左側,都有一個複選框,使用者可以選擇部分商品進行下單,而不一定是全部:
我們定義一個變量,記錄所有被選中的商品:
3.8.1.選中一個
我們給商品前面的複選框與selected綁定,并且指定其值為目前購物車商品:
3.8.2.初始化全選
我們在加載完成購物車查詢後,初始化全選:
3.8.3.總價格
然後編寫一個計算屬性,計算出選中商品總價格:
computed: {
totalPrice() {
return ly.formatPrice(this.selected.reduce((c1, c2) => c1 + c2.num * c2.price, 0));
}
}
在頁面中展示總價格:
效果:
4.已登入購物車
接下來,我們完成已登入購物車。
在剛才的未登入購物車編寫時,我們已經預留好了編寫代碼的位置,邏輯也基本一緻。
4.1.添加登入校驗
購物車系統隻負責登入狀态的購物車處理,是以需要添加登入校驗,我們通過JWT鑒權即可實作。
4.1.1.引入JWT相關依賴
我們引入之前寫的鑒權工具:
leyou-auth-common
<dependency>
<groupId>com.leyou.auth</groupId>
<artifactId>leyou-auth-common</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
4.1.2.配置公鑰
leyou:
jwt:
pubKeyPath: C:/tmp/rsa/rsa.pub # 公鑰位址
cookieName: LY_TOKEN # cookie的名稱
4.1.3.加載公鑰
代碼:
@ConfigurationProperties(prefix = "leyou.jwt")
public class JwtProperties {
private String pubKeyPath;// 公鑰
private PublicKey publicKey; // 公鑰
private String cookieName;
private static final Logger logger = LoggerFactory.getLogger(JwtProperties.class);
@PostConstruct
public void init(){
try {
// 擷取公鑰和私鑰
this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
} catch (Exception e) {
logger.error("初始化公鑰失敗!", e);
throw new RuntimeException();
}
}
public String getPubKeyPath() {
return pubKeyPath;
}
public void setPubKeyPath(String pubKeyPath) {
this.pubKeyPath = pubKeyPath;
}
public PublicKey getPublicKey() {
return publicKey;
}
public void setPublicKey(PublicKey publicKey) {
this.publicKey = publicKey;
}
public String getCookieName() {
return cookieName;
}
public void setCookieName(String cookieName) {
this.cookieName = cookieName;
}
}
4.1.4.編寫攔截器
因為很多接口都需要進行登入,我們直接編寫SpringMVC攔截器,進行統一登入校驗。同時,我們還要把解析得到的使用者資訊儲存起來,以便後續的接口可以使用。
代碼:
public class LoginInterceptor extends HandlerInterceptorAdapter {
private JwtProperties jwtProperties;
// 定義一個線程域,存放登入使用者
private static final ThreadLocal<UserInfo> tl = new ThreadLocal<>();
public LoginInterceptor(JwtProperties jwtProperties) {
this.jwtProperties = jwtProperties;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 查詢token
String token = CookieUtils.getCookieValue(request, "LY_TOKEN");
if (StringUtils.isBlank(token)) {
// 未登入,傳回401
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
}
// 有token,查詢使用者資訊
try {
// 解析成功,證明已經登入
UserInfo user = JwtUtils.getInfoFromToken(token, jwtProperties.getPublicKey());
// 放入線程域
tl.set(user);
return true;
} catch (Exception e){
// 抛出異常,證明未登入,傳回401
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
tl.remove();
}
public static UserInfo getLoginUser() {
return tl.get();
}
}
注意:
- 這裡我們使用了
來存儲查詢到的使用者資訊,線程内共享,是以請求到達ThreadLocal
後可以共享UserController
- 并且對外提供了靜态的方法:
來擷取User資訊getLoginUser()
4.1.5.配置攔截器
配置SpringMVC,使過濾器生效:
@Configuration
@EnableConfigurationProperties(JwtProperties.class)
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private JwtProperties jwtProperties;
@Bean
public LoginInterceptor loginInterceptor() {
return new LoginInterceptor(jwtProperties);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor()).addPathPatterns("/**");
}
}
4.2.背景購物車設計
當使用者登入時,我們需要把購物車資料儲存到背景,可以選擇儲存在資料庫。但是購物車是一個讀寫頻率很高的資料。是以我們這裡選擇讀寫效率比較高的Redis作為購物車存儲。
Redis有5種不同資料結構,這裡選擇哪一種比較合适呢?Map<String, List>
- 首先不同使用者應該有獨立的購物車,是以購物車應該以使用者的作為key來存儲,Value是使用者的所有購物車資訊。這樣看來基本的
結構就可以了。k-v
- 但是,我們對購物車中的商品進行增、删、改操作,基本都需要根據商品id進行判斷,為了友善後期處理,我們的購物車也應該是
結構,key是商品id,value才是這個商品的購物車資訊。k-v
綜上所述,我們的購物車結構是一個雙層Map:Map<String,Map<String,String>>
- 第一層Map,Key是使用者id
- 第二層Map,Key是購物車中商品id,值是購物車資料
實體類:
public class Cart {
private Long userId;// 使用者id
private Long skuId;// 商品id
private String title;// 标題
private String image;// 圖檔
private Long price;// 加入購物車時的價格
private Integer num;// 購買數量
private String ownSpec;// 商品規格參數
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public Long getSkuId() {
return skuId;
}
public void setSkuId(Long skuId) {
this.skuId = skuId;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getImage() {
return image;
}
public void setImage(String image) {
this.image = image;
}
public Long getPrice() {
return price;
}
public void setPrice(Long price) {
this.price = price;
}
public Integer getNum() {
return num;
}
public void setNum(Integer num) {
this.num = num;
}
public String getOwnSpec() {
return ownSpec;
}
public void setOwnSpec(String ownSpec) {
this.ownSpec = ownSpec;
}
}
4.3.添加商品到購物車
4.3.1.頁面發起請求
已登入情況下,向背景添加購物車:
ly.http.post("/cart", {skuId: this.sku.id, num: this.num}).then(res=>{
window.location = "http://www.leyou.com/cart.html";
})
這裡發起的是Json請求。那麼我們背景也要以json接收。
4.3.2.編寫controller
先分析一下:
- 請求方式:新增,肯定是Post
- 請求路徑:/cart ,這個其實是Zuul路由的路徑,我們可以不管
- 請求參數:Json對象,包含skuId和num屬性
- 傳回結果:無
@Controller
public class CartController {
@Autowired
private CartService cartService;
/**
* 添加購物車
*
* @return
*/
@PostMapping
public ResponseEntity<Void> addCart(@RequestBody Cart cart) {
this.cartService.addCart(cart);
return ResponseEntity.ok().build();
}
}
在leyou-gateway中添加路由配置:
4.3.3.CartService
這裡我們不通路資料庫,而是直接操作Redis。基本思路:
- 先查詢之前的購物車資料
- 判斷要添加的商品是否存在
- 存在:則直接修改數量後寫回Redis
- 不存在:建立一條資料,然後寫入Redis
代碼:
@Service
public class CartService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private GoodsClient goodsClient;
static final String KEY_PREFIX = "leyou:cart:uid:";
static final Logger logger = LoggerFactory.getLogger(CartService.class);
public void addCart(Cart cart) {
// 擷取登入使用者
UserInfo user = LoginInterceptor.getLoginUser();
// Redis的key
String key = KEY_PREFIX + user.getId();
// 擷取hash操作對象
BoundHashOperations<String, Object, Object> hashOps = this.redisTemplate.boundHashOps(key);
// 查詢是否存在
Long skuId = cart.getSkuId();
Integer num = cart.getNum();
Boolean boo = hashOps.hasKey(skuId.toString());
if (boo) {
// 存在,擷取購物車資料
String json = hashOps.get(skuId.toString()).toString();
cart = JsonUtils.parse(json, Cart.class);
// 修改購物車數量
cart.setNum(cart.getNum() + num);
} else {
// 不存在,新增購物車資料
cart.setUserId(user.getId());
// 其它商品資訊,需要查詢商品服務
Sku sku = this.goodsClient.querySkuById(skuId);
cart.setImage(StringUtils.isBlank(sku.getImages()) ? "" : StringUtils.split(sku.getImages(), ",")[0]);
cart.setPrice(sku.getPrice());
cart.setTitle(sku.getTitle());
cart.setOwnSpec(sku.getOwnSpec());
}
// 将購物車資料寫入redis
hashOps.put(cart.getSkuId().toString(), JsonUtils.serialize(cart));
}
}
需要引入leyou-item-interface依賴:
<dependency>
<groupId>com.leyou.item</groupId>
<artifactId>leyou-item-interface</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
4.3.4.GoodClient
參照搜尋工程,添加GoodClient,提供根據id查詢sku的接口:
@FeignClient("item-service")
public interface GoodsClient extends GoodsApi {
}
在leyou-item-service中的GoodsController添加方法:
@GetMapping("sku/{id}")
public ResponseEntity<Sku> querySkuById(@PathVariable("id")Long id){
Sku sku = this.goodsService.querySkuById(id);
if (sku == null){
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return ResponseEntity.ok(sku);
}
在leyou-item-service中的GoodsService添加方法:
public Sku querySkuById(Long id) {
return this.skuMapper.selectByPrimaryKey(id);
}
4.3.5.結果
4.4.查詢購物車
4.4.1.頁面發起請求
購物車頁面:cart.html
4.4.2.背景實作
Controller
/**
* 查詢購物車清單
*
* @return
*/
@GetMapping
public ResponseEntity<List<Cart>> queryCartList() {
List<Cart> carts = this.cartService.queryCartList();
if (carts == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}
return ResponseEntity.ok(carts);
}
Service
public List<Cart> queryCartList() {
// 擷取登入使用者
UserInfo user = LoginInterceptor.getLoginUser();
// 判斷是否存在購物車
String key = KEY_PREFIX + user.getId();
if(!this.redisTemplate.hasKey(key)){
// 不存在,直接傳回
return null;
}
BoundHashOperations<String, Object, Object> hashOps = this.redisTemplate.boundHashOps(key);
List<Object> carts = hashOps.values();
// 判斷是否有資料
if(CollectionUtils.isEmpty(carts)){
return null;
}
// 查詢購物車資料
return carts.stream().map(o -> JsonUtils.parse(o.toString(), Cart.class)).collect(Collectors.toList());
}
4.4.3.測試
4.5.修改商品數量
4.5.1.頁面發起請求
4.5.2.背景實作
Controller
@PutMapping
public ResponseEntity<Void> updateNum(@RequestBody Cart cart){
this.cartService.updateCarts(cart);
return ResponseEntity.noContent().build();
}
Service
public void updateCarts(Cart cart) {
// 擷取登陸資訊
UserInfo userInfo = LoginInterceptor.getUserInfo();
String key = LEYOU_CART_KEY + userInfo.getId();
// 擷取hash操作對象
BoundHashOperations<String, Object, Object> hashOperations = this.redisTemplate.boundHashOps(key);
// 擷取購物車資訊
String cartJson = hashOperations.get(cart.getSkuId().toString()).toString();
Cart cart1 = JsonUtils.parse(cartJson, Cart.class);
// 更新數量
cart1.setNum(cart.getNum());
// 寫入購物車
hashOperations.put(cart.getSkuId().toString(), JsonUtils.serialize(cart1));
}
4.6.删除購物車商品
4.6.1.頁面發起請求
ly.http.delete("/cart/" + this.carts[index].skuId).then(() => {
this.carts.splice(index,1);
})
注意:背景成功響應後,要把頁面的購物車中資料也删除
4.6.2.背景實作
Controller
@DeleteMapping("{skuId}")
public ResponseEntity<Void> deleteCart(@PathVariable("skuId") String skuId) {
this.cartService.deleteCart(skuId);
return ResponseEntity.ok().build();
}
Service
public void deleteCart(String skuId) {
// 擷取登入使用者
UserInfo user = LoginInterceptor.getLoginUser();
String key = KEY_PREFIX + user.getId();
BoundHashOperations<String, Object, Object> hashOps = this.redisTemplate.boundHashOps(key);
hashOps.delete(skuId);
}
5.登入後購物車合并(作業)
已完成,細節需要自己了解,注意細節:
當跳轉到購物車頁面,查詢購物車清單前,需要判斷使用者登入狀态,
- 如果登入:
- 首先檢查使用者的LocalStorage中是否有購物車資訊,
- 如果有,則送出到背景儲存,
- 清空LocalStorage
- 如果未登入,直接查詢即可
5.1 修改前台
5.2 修改背景
Controller
/**
* 合并購物車
*
* @return
*/
@PutMapping("add")
public ResponseEntity<Void> addCartToUser(@RequestBody Cart cart) {
this.cartService.addCart(cart);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
這裡service實作方法直接調用添加購物車方法。