關于商城中購物車功能,天貓是必須登錄才能將商品加入到購物車,京東則可以在不登錄狀態下也可以加入購物車,這里就模仿京東購物車功能。
購物車工程搭建:
e3-cart(pom)
|–e3-cart-interface(jar)
|-e3-cart-service(war)
e3-cart-web(war)
參照”redis實現單點登錄系統”搭建
需求:
商品詳情頁面如下:
選擇好商品,確定數量后,點擊“加入購物車”按鈕,發送請求。
請求地址:8090/cart/add/{itemId}.html,參數:商品id跟商品數量
返回邏輯視圖:”cartSuccess”;
一、未登錄狀態下購物車功能實現
1、未登錄狀態下添加商品到購物車
在不登陸的情況下也可以添加購物車。把購物車信息寫入cookie。
優點:
1、不占用服務端存儲空間
2、用戶體驗好。
3、代碼實現簡單。
缺點:
1、cookie中保存的容量有限。最大4k
2、把購物車信息保存在cookie中,更換設備購物車信息不能同步。
分析:頁面傳來的是商品id跟商品數量
(1) 從cookie中獲取商品列表信息(單獨提出來寫成個通用的方法)
(2) 遍歷購物車列表,判斷需要添加的商品在購物車列表是否存在
(3) 商品存在的話,那么取出該商品原來的數量+添加的數量作為該商品現在的數量
(4) 如果商品不存在,那么調用服務,根據傳來的商品id查詢商品數量,設置商品的數量為頁面傳來的數量,取商品的第一張圖片(購物車列表只展示一張圖片)。
(5) 把修改后的購物車列表重新存入到cookie中
(6) 返回邏輯視圖”cartSuccess”
實現:
在表現層工程e3-cart-web中引用商品服務工程提供的服務
<!-- 加載配置文件 -->
<context:property-placeholder location="classpath:conf/resource.properties" />
<context:component-scan base-package="cn.e3mall.cart.controller" />
<mvc:annotation-driven />
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
<!-- 引用dubbo服務 -->
<dubbo:application name="e3-cart-web"/>
<dubbo:registry protocol="zookeeper" address="192.168.25.128:2181"/>
<dubbo:reference interface="cn.e3mall.service.ItemService" id="itemService" />
ItemService提供了根據id獲取商品信息的方法:getItemById(Long itemId)
@Controller
public class CartController {
@Autowired
private ItemService itemService;
@Value("${COOKIE_MAX_TIME}")
private Integer COOKIE_MAX_TIME;
/*
* 1.未登錄狀態下添加購物車商品
*/
@RequestMapping("/cart/add/{itemId}")
public String addCartNum(@PathVariable Long itemId, Integer num,
HttpServletRequest request,HttpServletResponse response){
//獲取購物車列表
List<TbItem> cartList = getCartListFromCookie(request);
//判斷購物車中是否有該商品
boolean flag = false;
for (TbItem tbItem : cartList) {
if(tbItem.getId()==itemId.longValue()){
flag = true;
//存在該商品,數量相加
tbItem.setNum(tbItem.getNum()+num);
//跳出循環
break;
}
}
if(!flag){
//沒有的話,調用服務查詢該商品
TbItem tbItem = itemService.getItemById(itemId);
//設置數量
tbItem.setNum(num);
//取一張圖片
String image = tbItem.getImage();
if(StringUtils.isNotBlank(image)){
tbItem.setImage(image.split(",")[0]);
}
//商品添加到購物車列表
cartList.add(tbItem);
}
//購物車信息寫入cookie
CookieUtils.setCookie(request, response, "cart1",
JsonUtils.objectToJson(cartList), COOKIE_MAX_TIME, true);
//返回邏輯視圖
return "cartSuccess";
}
/*
*從cookie中獲取購物車列表
*/
public List<TbItem> getCartListFromCookie(HttpServletRequest request){
String string = CookieUtils.getCookieValue(request, "cart1", true);
//判斷是否為空
if(StringUtils.isBlank(string)){
//空的話也不能返回null
return new ArrayList<>();
}
//轉為商品列表
List<TbItem> list = JsonUtils.jsonToList(string, TbItem.class);
return list;
}
}
其中商品實體類TbItem里面的屬性image存放的是多張照片。
COOKIE_MAX_TIME便是cookie中cart1最大存在時間,true表示采用utf-8編碼
測試:
其實并不能看出來效果。展示購物車列表功能實現后就能看到了。
2、展示購物車列表
單擊“去購物車結算按鈕”向服務端發送請求,服務端應該返回邏輯視圖”cart”
請求地址:8090/cart/cart.html
返回邏輯視圖:”cart”也就是購物車列表頁面
實現:同樣是在CartController中添加
/*
* 2.未登錄狀態下展示商品列表
*/
@RequestMapping("/cart/cart")
public String showCartList(HttpServletRequest request){
//獲取購物車列表
List<TbItem> cartList = getCartListFromCookie(request);
//綁定參數
request.setAttribute("cartList", cartList);
//返回邏輯視圖
return "cart";
}
注:cartList是根據cart.jsp的需要綁定的。該頁面拿到cartList后會進行遍歷,取各個商品的信息。
測試:
3、為登錄狀態下購物車列表頁面修改商品數量
購物車列表頁面單擊”+”,”-”會向服務端發送ajax請求。
頁面需要根據調整的數量重新顯示商品總計(已經實現了也就是輸入框的值*價格)和小計(用js,待實現)
服務端要求修改cookie中對應商品的數量。
請求地址:/cart/update/num/{itemId}/{num}
參數:商品id,商品數量
返回結果:E3Result
/*
* 未登錄狀態下更新商品數量
*/
@RequestMapping("/cart/update/num/{itemId}/{num}")
@ResponseBody
public E3Result updateCartNum(@PathVariable Long itemId,@PathVariable Integer num,
HttpServletRequest request,HttpServletResponse response){
//獲取購物車列表
List<TbItem> cartList = getCartListFromCookie(request);
//取所選擇的需要更新的商品
for (TbItem tbItem : cartList) {
if(tbItem.getId()==itemId.longValue()){
//更新商品數量
tbItem.setNum(num);
//跳出循環
break;
}
}
//購物車信息寫入cookie
CookieUtils.setCookie(request, response, "cart1",
JsonUtils.objectToJson(cartList), COOKIE_MAX_TIME, true);
return E3Result.ok();
}
測試:
注:商品總金額的js沒有去寫所以還是只顯示單價。
E3Result為自定義響應結構
public class E3Result implements Serializable{
// 定義jackson對象
private static final ObjectMapper MAPPER = new ObjectMapper();
// 響應業務狀態
private Integer status;
// 響應消息
private String msg;
// 響應中的數據
private Object data;
public static E3Result build(Integer status, String msg, Object data) {
return new E3Result(status, msg, data);
}
public static E3Result ok(Object data) {
return new E3Result(data);
}
public static E3Result ok() {
return new E3Result(null);
}
public E3Result() {
}
public static E3Result build(Integer status, String msg) {
return new E3Result(status, msg, null);
}
public E3Result(Integer status, String msg, Object data) {
this.status = status;
this.msg = msg;
this.data = data;
}
public E3Result(Object data) {
this.status = 200;
this.msg = "OK";
this.data = data;
}
get、set方法
}
4、未登錄狀態下刪除購物車商品
請求地址:/cart/delete/{itemId}
請求參數:商品id
響應:重定向到購物車列表。
實現:
(1)從cookie中獲取購物車列表
(2)遍歷,查找到要刪除的商品
(3)將該商品從購物車列表移除
(4)更新后的購物車列表重新寫入cookie
(5)重定向到購物車列表頁面
/*
* 未登錄狀態下刪除購物車商品
*/
@RequestMapping("/cart/delete/{itemId}")
public String deleteCartById(@PathVariable Long itemId,
HttpServletRequest request,HttpServletResponse response){
//獲取商品列表
List<TbItem> cartList = getCartListFromCookie(request);
//遍歷商品列表,找到該商品
for (TbItem tbItem : cartList) {
if(tbItem.getId() == itemId.longValue()){
//刪除該商品
cartList.remove(tbItem);
break;
}
}
//購物車信息寫入cookie
CookieUtils.setCookie(request, response, "cart1",
JsonUtils.objectToJson(cartList), COOKIE_MAX_TIME, true);
//重定向到列表頁面
return "redirect:/cart/cart.html";
}
測試:
上面的圖,點擊刪除后
二、登錄狀態下購物車功能的實現
功能分析:
1、購物車數據保存的位置:
未登錄狀態下,把購物車數據保存到cookie中。
登錄狀態下,需要把購物車數據保存到服務端。需要永久保存,可以保存到數據庫中。可以把購物車數據保存到redis中。
2、redis使用的數據類型
a) 使用hash數據類型
b) Hash的key應該是用戶id。Hash中的field是商品id,value可以是把商品信息轉換成json
3、添加購物車
登錄狀態下直接把商品數據保存到redis中。
未登錄狀態保存到cookie中。
4、如何判斷是否登錄?
a) 從cookie中取token
b) 取不到未登錄
c) 取到token,到redis中查詢token是否過期。
d) 如果過期,未登錄狀態
e) 沒過期登錄狀態。
1、登錄攔截器
幾乎在購物車所有功能執行 都要判斷用戶是否登錄。利用aop思想,應該編寫個攔截器,來判斷用戶是否登錄。登錄的話用戶信息需要存在request域中
(1) 從cookie中取token
(2) 判斷token是否存在
(3) 不存在,說明用于未登錄,放行
(4) 如果token存在,調用服務,根據token從redis中取用戶信息
(5) 取不到用戶信息,說明已經過期,放行
(6) 取到了用戶信息,說明用戶已經登錄,用戶信息存到request中
(7) 放行
實現:
首先需要在購物車系統表現層工程中(e3-cart-web)調用單點登錄系統(sso)的服務,以及攔截器的配置。
<!-- 加載配置文件 -->
<context:property-placeholder location="classpath:conf/resource.properties" />
<context:component-scan base-package="cn.e3mall.cart.controller" />
<mvc:annotation-driven />
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
<!-- 攔截器配置 -->
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="cn.e3mall.cart.interceptor.LoginInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
<!-- 引用dubbo服務 -->
<dubbo:application name="e3-cart-web"/>
<dubbo:registry protocol="zookeeper" address="192.168.25.128:2181"/>
<dubbo:reference interface="cn.e3mall.service.ItemService" id="itemService" />
<dubbo:reference interface="cn.e3mall.sso.service.TokenService" id="tokenService" />
/*
* 用戶登錄處理
*/
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private TokenService tokenService;
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
//前處理,執行handler之前執行此方法
//返回true:放行 false:攔截
//1.從cookie中取token
String token = CookieUtils.getCookieValue(request, "token");
//2.如果沒有token,未登錄狀態
if(StringUtils.isBlank(token)){
return true;
}
//3.如果取到token,需要調用sso系統的服務,根據token取用戶信息
E3Result e3Result = tokenService.getUserByToken(token);
if (e3Result.getStatus()!=200){
//4.沒有取到用戶信息,登錄已經過期,直接放行
return true;
}
//5.取到用戶信息。登錄狀態。
TbUser user = (TbUser) e3Result.getData();
//6.把用戶信息放到request中,只需要在controller中判斷request中是否包含user信息。
request.setAttribute("user", user);
return true;
}
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
//handler執行之后,返回modelAndView之前
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
//完成處理,返回modelAndView之后(已經響應了)
//可以再次處理異常
}
}
攔截器寫完之后,對于購物車功能只需要在表現層判斷用戶是否登錄,從而進行不同的處理。
2、登錄狀態下,商品添加功能實現
(1)、服務層e3-cart-service中:
服務層用到了redis,所以需要將redis和spring整合。
<!-- 連接redis單機版 -->
<bean id="jedisClientPool" class="cn.e3mall.common.jedis.JedisClientPool">
<property name="jedisPool" ref="jedisPool"></property>
</bean>
<bean id="jedisPool" class="redis.clients.jedis.JedisPool">
<!-- 一定要用name,構造方法太多用index容易錯 -->
<constructor-arg name="host" value="192.168.25.128"/>
<constructor-arg name="port" value="6379"/>
</bean>
JedisClient只是自己對jedis操作redis的api的封裝。服務層當然還得添加其他配置,如組件掃描,引入數據源,事務。
/*
* 購物車處理服務
*/
@Service
public class CartServiceImpl implements CartService{
@Autowired
private JedisClient jedisClient;
@Value("${REDIS_CART_PRE}")//屬性配置文件中,值為cart1
private String REDIS_CART_PRE;
@Autowired
private TbItemMapper itemMapper;
public E3Result addCart(Long userId, Long itemId, int num) {
//向redis中添加購物車
//數據類型是hash key:用戶id field:商品id value:商品信息
//判斷商品是否存在
Boolean hexists = jedisClient.hexists(REDIS_CART_PRE+":"+userId, itemId+"");
if(hexists){
//如果存在,數量相加
String json = jedisClient.hget(REDIS_CART_PRE+":"+userId, itemId+"");
//把json轉換成TbItem
TbItem tbItem = JsonUtils.jsonToPojo(json, TbItem.class);
tbItem.setNum(tbItem.getNum()+num);
//寫回redis
jedisClient.hset(REDIS_CART_PRE+":"+userId, itemId+"",JsonUtils.objectToJson(tbItem));
return E3Result.ok();
}
//如果不存在,根據商品id取商品信息,服務層盡量別相互調用
TbItem item = itemMapper.selectByPrimaryKey(itemId);
//設置購物車數量
item.setNum(num);
//取一張圖片
String image = item.getImage();
if(StringUtils.isNotBlank(image)){
item.setImage(image.split(",")[0]);
}
//添加到購物車列表
jedisClient.hset(REDIS_CART_PRE+":"+userId, itemId+"",JsonUtils.objectToJson(item));
//返回成功
return E3Result.ok();
}
}
發布服務:
<context:component-scan base-package="cn.e3mall.cart.service"/>
<!-- 使用dubbo發布服務 -->
<!-- 提供方應用信息,用于計算依賴關系 -->
<dubbo:application name="e3-cart" />
<dubbo:registry protocol="zookeeper"
address="192.168.25.128:2181" />
<!-- 用dubbo協議在20880端口暴露服務 -->
<dubbo:protocol name="dubbo" port="20884" /><!-- 一個服務對應一個端口 -->
<!-- 聲明需要暴露的服務接口 -->
<dubbo:service interface="cn.e3mall.cart.service.CartService" ref="cartServiceImpl" timeout="600000"/>
(2)、表現層工程e3-cart-web中
調用e3-car-service剛發布的服務
<!-- 引用dubbo服務 -->
<dubbo:application name="e3-cart-web"/>
<dubbo:registry protocol="zookeeper" address="192.168.25.128:2181"/>
<dubbo:reference interface="cn.e3mall.service.ItemService" id="itemService" />
<dubbo:reference interface="cn.e3mall.sso.service.TokenService" id="tokenService" />
<dubbo:reference interface="cn.e3mall.cart.service.CartService" id="cartService" />
只需要再原來的添加商品功能中做判斷處理
@RequestMapping("/cart/add/{itemId}")
public String addCart(@PathVariable Long itemId, @RequestParam(defaultValue="1") Integer num,
HttpServletRequest request, HttpServletResponse response){
//判斷用戶是否為登錄狀態
TbUser user = (TbUser) request.getAttribute("user");
if(user != null){
//保存到服務端
cartService.addCart(user.getId(), itemId, num);
//返回邏輯視圖
return "cartSuccess";
}
//如果是登錄狀態,把購物車寫入redis
//如果未登錄使用cookie
...未登錄狀態下代碼
}
測試:
Tidy用戶登錄,買了一個thinkpad電腦,單擊加入購物車
查看redis,商品信息已經添加
2、登錄狀態下,商品列表展示
分析:
(1)從cookie中取購物車列表
(2)判斷用戶是否登錄
(3)用戶已經登錄的話,則調用服務層,合并cookie中的列表和redis中的列表。存入到redis中。
(4)同時刪除cookie中的購物車列表
(5)根據用戶id,調用服務查詢redis中所有的商品,返回購物車列表。
(6)未登錄狀態還是跟前面一樣
(7)將列表綁定到參數,返回購物車列表頁面。
在服務層e3-cart-service中
/*
* 合并購物車
*/
public E3Result mergeCart(Long userId, List<TbItem> itemList) {
//遍歷商品列表
//把列表添加到購物車
//判斷購物車中是否有此商品
//如果有,數量相加
//如果沒有添加新的商品
for (TbItem tbItem : itemList) {
//等同于上面的添加商品到redis中
addCart(userId, tbItem.getId(), tbItem.getNum());
}
return E3Result.ok();
}
/*
* 取購物車列表
*/
public List<TbItem> getCartList(long userId) {
//根據用戶id查詢購物車列表
List<String> jsonList = jedisClient.hvals(REDIS_CART_PRE+":"+userId);
List<TbItem> itemList = new ArrayList<>();
for (String string : jsonList) {
//創建一個TbItem
TbItem item = JsonUtils.jsonToPojo(string, TbItem.class);
//添加到列表
itemList.add(item);
}
return itemList;
}
表現層工程 e3-cart-web中
/*
* 展示購物車列表
*/
@RequestMapping("/cart/cart")
public String showCartList(HttpServletRequest request,HttpServletResponse response){
//從cookie中取購物車列表
List<TbItem> list = getCartListFromCookie(request);
//判斷用戶是否為登錄狀態
TbUser user = (TbUser) request.getAttribute("user");
//如果是登錄狀態
if(user!=null){
//從cookie中取購物車列表
//如果不為空,把cookie中的購物車商品和服務端的購物車商品合并。
cartService.mergeCart(user.getId(), list);
//把cookie中的購物車刪除
CookieUtils.deleteCookie(request, response, "cart");
//從服務端取購物車列表
list = cartService.getCartList(user.getId());
}
//未登錄狀態
//把列表傳遞給頁面
request.setAttribute("cartList", list);
//返回邏輯視圖
return "cart";
}
測試:
先不登錄狀態下添加商品都購物車,再登錄添加商品到購物車。
再登錄tidy賬號(之前買了個電腦放入到了購物車)
發現已經合并成功了,再看cookie中
發現購物車已經為空了。
也可以看下redis中,商品合并了
3、登錄狀態下修改購物車商品數量
分析
單擊”+”,”-”修改商品的數量的時候,要求redis中該商品的數量發生改變
(1) 根據用戶id,商品id從redis中取出對應的商品
(2) 設置商品的數量
(3) 該商品更新到redis中
(4) 返回E3Result
實現:
服務層e3-cart-service中
/*
* 登錄狀態下更新購物車數量
*/
public E3Result updateCartNum(Long userId, Long itemId, int num) {
//從redis中取商品信息
String json = jedisClient.hget(REDIS_CART_PRE+":"+userId, itemId+"");
//更新商品數量
TbItem tbItem = JsonUtils.jsonToPojo(json, TbItem.class);
tbItem.setNum(num);
//寫入redis
jedisClient.hset(REDIS_CART_PRE+":"+userId, itemId+"",JsonUtils.objectToJson(tbItem));
return E3Result.ok();
}
表現層工程e3-cart-web中
/*
* 更新購物車商品數量
*/
@RequestMapping("/cart/update/num/{itemId}/{num}")
@ResponseBody
public E3Result updateCartNum(@PathVariable Long itemId, @PathVariable Integer num,
HttpServletRequest request, HttpServletResponse response){
//判斷用戶是否為登錄狀態
TbUser user = (TbUser) request.getAttribute("user");
if (user != null){
cartService.updateCartNum(user.getId(), itemId, num);
return E3Result.ok();
}
//從cookie中取購物車列表
List<TbItem> cartList = getCartListFromCookie(request);
//遍歷商品列表找到對應的商品
for (TbItem tbItem : cartList) {
//包裝類型直接==比的是內存地址
if(tbItem.getId() == itemId.longValue()){
//跟新數量
tbItem.setNum(num);
break;
}
}
//把購物車列表寫回cookie
CookieUtils.setCookie(request, response, "cart",
JsonUtils.objectToJson(cartList), COOKIE_CART_EXPIRE, true);
//返回成功
return E3Result.ok();
}
5、登錄狀態下,刪除購物車商品
分析
單擊刪除的時候,刪除redis中該商品。重定向到列表頁面
(1) 直接用jedisClient的del的方法根據用戶id跟商品id 商品
(2) 返回成功
服務層e3-cart-service中
/*
* 登錄狀態下刪除
*/
public E3Result deleteCartItem(long userId, long itemId) {
//刪除購物車商品
jedisClient.hdel(REDIS_CART_PRE+":"+userId, itemId+"");
return E3Result.ok();
}
表現層工程e3-cart-web中
在原先的刪除方法中添加即可
/*
* 從購物車刪除商品
*/
@RequestMapping("/cart/delete/{itemId}")
public String deleteCartItem(@PathVariable Long itemId,HttpServletRequest request,
HttpServletResponse response){
//判斷用戶是否為登錄狀態
TbUser user = (TbUser) request.getAttribute("user");
if (user != null){
cartService.deleteCartItem(user.getId(), itemId);
return "redirect:/cart/cart.html";
}
未登錄狀態下刪除購物車
...
}
修改刪除測試:
初始情況
現在:刪除手機,筆記本的數量改為2,操作后頁面跟redis中如下