1. 程式人生 > >Cookie及Redis在商城購物車系統中的使用

Cookie及Redis在商城購物車系統中的使用

關於商城中購物車功能,天貓是必須登入才能將商品加入到購物車,京東則可以在不登入狀態下也可以加入購物車,這裡就模仿京東購物車功能。
購物車工程搭建:
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中如下
這裡寫圖片描述
這裡寫圖片描述