- springboot 工程
- 主要說明購物車流程(故將登錄用戶信息保存至session)
- 未登錄時 將用戶臨時key 保存至cookie
- 有不足之處 請大佬指點
項目源碼: https://github.com/youxiu326/sb_shopping_cart
項目結構:

package com.youxiu326.common; public class JsonResult { private String code; private String message; private Object data; public JsonResult() { this.code = "200"; this.message = "操作成功"; } public JsonResult success(String message){ this.code = "200"; this.message = message; return this; } public JsonResult error(String message){ this.code = "400"; this.message = message; return this; } public JsonResult error(){ this.code = "400"; this.message = "操作失敗"; return this; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public Object getData() { return data; } public void setData(Object data) { this.data = data; } }

package com.youxiu326.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import javax.annotation.PostConstruct; /** * 防止redis 中文亂碼 */ @Configuration public class RedisConfig { @Autowired private RedisTemplate<Object, Object> redisTemplate; @PostConstruct public void initRedisTemplate() { redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); } }

package com.youxiu326.entity; import java.io.Serializable; /** * 用戶 */ public class Account implements Serializable { private String id = "youxiu326"; private String code = "test"; private String pwd = "123456"; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getPwd() { return pwd; } public void setPwd(String pwd) { this.pwd = pwd; } }

package com.youxiu326.entity; import java.io.Serializable; import java.util.ArrayList; import java.util.List; /** * 購物車對象 一個購物車由n個CartItem組成 */ public class ShoppingCart implements Serializable { public static final String unLoginKeyPrefix="TMP_"; public static final String loginKeyPrefix="USER_"; private String key=""; private List<CartItem> cartItems = new ArrayList<>();//防止空指針 public ShoppingCart(){} public ShoppingCart(String key) { this.key = key; } public String getKey() { return key; } public void setKey(String key) { this.key = key; } public List<CartItem> getCartItems() { return cartItems; } public void setCartItems(List<CartItem> cartItems) { this.cartItems = cartItems; } }

package com.youxiu326.entity; import java.io.InputStream; import java.io.Serializable; import java.util.Objects; /** * 購物實體 */ public class CartItem implements Serializable { private String code; private Integer quantity; public String getCode() { return code; } public void setCode(String code) { this.code = code; } public Integer getQuantity() { return quantity; } public void setQuantity(Integer quantity) { this.quantity = quantity; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; CartItem cartItem = (CartItem) o; return Objects.equals(code, cartItem.code); } @Override public int hashCode() { return Objects.hash(code); } }

# post server.port=8888 # redis spring.redis.host=youxiu326.xin spring.redis.port=6379 # thymeleaf spring.thymeleaf.cache=false

<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <base th:href="${#httpServletRequest.getContextPath()+'/'}"> <meta charset="UTF-8"> <title>測試未登陸基於redis實現購物車功能</title> </head> <body> <h2>商品列表</h2> <table> <tr> <td>商品編號</td> <td>商品數量</td> </tr> <tr th:each="cart,iterStat : ${cartItems}"> <th scope="row" th:text="${cart.code}">1</th> <td th:text="${cart.quantity}">quantity</td> <!-- <td >--> <!-- <img th:src="${cart.webLogo}"/>--> <!-- </td>--> <!-- <td th:text="${iterStat.index}">index</td>--> </tr> </table> <br/> <form action="#" method="post"> <table> <tr> <td>商品編號:</td> <td><input type="text" id="code" name="code" value="youxiu001"></td> </tr> <tr> <td>數量:</td> <td><input id="quantity" name="quantity" value="1"></td> </tr> <tr> <td colspan="1"><button type="button" onclick="add()">加購</button></td> <td colspan="1"><button type="button" onclick="remove()">減購</button></td> </tr> </table> </form> </body> <script src="/jquery-1.11.3.min.js"></script> <!--<script th:src="@{/jquery-1.11.3.min.js}"></script>--> <script> function add(){ $.ajax({ type: 'POST', url: "/shopping/add", data: {"code":$("#code").val(),"quantity":$("#quantity").val()}, // dataType: "json", success: function(response){ if(response.code=="200"){ window.location.reload(); }else{ alert(response.message); } }, error:function(response){ alert(response.message); console.log(response); } }); } function remove(){ $.ajax({ type: 'POST', url: "/shopping/remove", data: {"code":$("#code").val(),"quantity":$("#quantity").val()}, // dataType: "json", success: function(response){ if(response.code=="200"){ window.location.reload(); }else{ alert(response.message); } }, error:function(response){ alert("失敗"); console.log(response); } }); } </script> </html>

<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <base th:href="${#httpServletRequest.getContextPath()+'/'}"> <meta charset="UTF-8"> <title>登陸界面</title> </head> <body> <div th:if="${session.account == null}">[未登陸]</div> <div th:if="${session.account != null}"> <div th:text="${session.account.code}"></div> </div> <form action="#" method="post"> <table> <tr> <td>用戶:</td> <td><input type="text" id="code" name="code" value="test"></td> </tr> <tr> <td>密碼:</td> <td><input id="pwd" name="pwd" value="youxiu326"></td> </tr> <tr> <td colspan="1"><button type="button" onclick="login()">登陸</button></td> <td colspan="1"><button type="button" onclick="logout()">登出</button></td> </tr> </table> </form> <br/> <a target="_blank" href="/shopping/index">去購物車頁面</a> </body> <script src="/jquery-1.11.3.min.js"></script> <!--<script th:src="@{/jquery-1.11.3.min.js}"></script>--> <script> function login(){ $.ajax({ type: 'POST', url: "/login", data: {"code":$("#code").val(),"pwd":$("#pwd").val()}, // dataType: "json", success: function(response){ if(response.code=="200"){ alert(response.message); window.location.reload(); }else{ alert(response.message); } }, error:function(response){ alert(response.message); console.log(response); } }); } function logout(){ $.ajax({ type: 'POST', url: "/logout", data: {"code":$("#code").val()}, // dataType: "json", success: function(response){ if(response.code=="200"){ alert(response.message); window.location.reload(); }else{ alert(response.message); } }, error:function(response){ alert("失敗"); console.log(response); } }); } </script> </html>

package com.youxiu326.controller; import com.youxiu326.common.JsonResult; import com.youxiu326.entity.Account; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; /** * 登陸接口 */ @Controller public class LoginCtrl { @GetMapping("/") public String toLogin(HttpServletRequest req){ return "login"; } @PostMapping("/login") @ResponseBody public JsonResult login(HttpServletRequest req,Account account){ JsonResult result = new JsonResult(); if (StringUtils.isBlank(account.getCode()) || StringUtils.isBlank(account.getPwd())){ result.error("賬戶或密碼為空"); return result; } //創建登陸用戶賬戶 【主要邏輯是購物車 登陸用戶id 就是用戶code】 account.setId(account.getCode()); //將用戶保存至session req.getSession().setAttribute("account",account); return result.success("登陸成功"); } @PostMapping("/logout") @ResponseBody public JsonResult logout(HttpServletRequest req,Account account){ JsonResult result = new JsonResult(); if (StringUtils.isBlank(account.getCode())){ result.error("賬戶為空"); return result; } req.getSession().removeAttribute("account"); return result.success("登出成功"); } }
主要業務流程service:
package com.youxiu326.service.impl; import com.youxiu326.common.JsonResult; import com.youxiu326.entity.Account; import com.youxiu326.entity.CartItem; import com.youxiu326.entity.ShoppingCart; import com.youxiu326.service.ShoppingCartService; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.HashOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.web.util.WebUtils; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.UUID; @Service public class ShoppingCartServiceImpl implements ShoppingCartService { @Autowired private RedisTemplate redisTemplate; /** * 獲得用戶key * * 1.用戶未登錄情況下第一次進入購物車 -> 生成key 保存至cookie中 * 2.用戶未登錄情況下第n進入購物車 -> 從cookie中取出key * 3.用戶登錄情況下 -> 根據用戶code生成key * 4.用戶登錄情況下並且cookie中存在key-> 從cookie取的的key從緩存取得購物車 合並至 * 用戶code生成key的購物車中去 ,這樣后面才能根據用戶code 取得正確的購物車 * * @param req * @param resp * @param account * @return */ @Override public String getKey(HttpServletRequest req, HttpServletResponse resp, Account account) { //https://github.com/youxiu326/sb_shiro_session.git String key = null; //最終返回的key String tempKey = ""; //用來存儲cookie中的臨時key, Cookie cartCookie = WebUtils.getCookie(req, "shoopingCart"); if(cartCookie!=null){ //獲取Cookie中的key key = cartCookie.getValue(); tempKey = cartCookie.getValue(); } if(StringUtils.isBlank(key)){ key = ShoppingCart.unLoginKeyPrefix + UUID.randomUUID(); if (account!=null) key = ShoppingCart.loginKeyPrefix + account.getId(); Cookie cookie = new Cookie("shoopingCart",key); cookie.setMaxAge(-1); cookie.setPath("/"); resp.addCookie(cookie); }else if (StringUtils.isNotBlank(key) && account!=null){//⑵ key = ShoppingCart.loginKeyPrefix + account.getId(); if (tempKey.startsWith(ShoppingCart.unLoginKeyPrefix)){//⑴ //1.滿足cookie中取得的key 為未登錄時的key //2.滿足當前用戶已經登錄 //3.合並未登錄時用戶所添加的購物車商品⑷ mergeCart(tempKey,account);//⑶ } } return key; } /** * 合並購物車 返回最終購物車 * @param tempKey */ public ShoppingCart mergeCart(String tempKey,Account account) { ShoppingCart loginCart = null; String loginkey = null; // 從redis取出用戶緩存購物車數據 HashOperations<String, String, ShoppingCart> vos = redisTemplate.opsForHash(); ShoppingCart unLoginCart = vos.get("CACHE_SHOPPINGCART", tempKey); if (unLoginCart == null){ unLoginCart = new ShoppingCart(tempKey); } if (account != null && tempKey.startsWith(ShoppingCart.unLoginKeyPrefix)) {//⑵ //如果用戶登錄 並且 當前是未登錄的key loginkey = ShoppingCart.loginKeyPrefix + account.getId(); loginCart = mergeCart(loginkey, account); if (null != unLoginCart.getCartItems()) {//⑴ if (null != loginCart.getCartItems()) { //滿足未登錄時的購物車不為空 並且 當前用戶已經登錄 //進行購物車合並 for (CartItem cv : unLoginCart.getCartItems()) { long count = loginCart.getCartItems().stream().filter(it->it.getCode().equals(cv.getCode())).count(); if(count == 0 ){//沒有重復的商品 則直接將商品加入購物車 loginCart.getCartItems().add(cv); }else if(count == 1){//出現重復商品 修改數量 CartItem c = loginCart.getCartItems().stream().filter(it->it.getCode().equals(cv.getCode())).findFirst().orElse(null); c.setQuantity(c.getQuantity()+1); } } } else { //如果當前登錄用戶的購物車為空則 將未登錄時的購物車合並 loginCart.setCartItems(unLoginCart.getCartItems()); } unLoginCart = loginCart; //【刪除臨時key】 vos.delete("CACHE_SHOPPINGCART",tempKey); //【將合並后的購物車數據 放入loginKey】//TMP_4369f86d-c026-4b1b-8fec-f3c69f6ffac5 vos.put("CACHE_SHOPPINGCART",loginkey, unLoginCart); } } return unLoginCart; } /** * 添加購物車 * @param req * @param resp * @param account 登陸用戶信息 * @param item 添加的購物車商品信息 包含商品code 商品加購數量 * @return */ public JsonResult addCart(HttpServletRequest req, HttpServletResponse resp,Account account,CartItem item){ JsonResult result = new JsonResult(); String key = getKey(req, resp,account);//得到最終key ShoppingCart cacheCart = mergeCart(key,account);//根據key取得最終購物車對象 if(StringUtils.isNotBlank(item.getCode()) && item.getQuantity()>0){ //TODO 進行一系列 商品上架 商品code是否正確 最大購買數量.... if(false){ return result.error(); } long count = 0; if(null != cacheCart.getCartItems()) { count = cacheCart.getCartItems().stream().filter(it->it.getCode().equals(item.getCode())).count(); } if (count==0){ //之前購物車無該商品記錄 則直接添加 cacheCart.getCartItems().add(item); }else { //否則將同一商品數量相加 CartItem c = cacheCart.getCartItems().stream().filter(it->it.getCode().equals(item.getCode())).findFirst().orElse(null); c.setQuantity(c.getQuantity()+item.getQuantity()); } } //【將合並后的購物車數據 放入loginKey】 HashOperations<String,String,ShoppingCart> vos = redisTemplate.opsForHash(); vos.put("CACHE_SHOPPINGCART",key, cacheCart); result.setData(cacheCart); return result; } /** * 移除購物車 * @param req * @param resp * @param account * @param item * @return */ public JsonResult removeCart(HttpServletRequest req, HttpServletResponse resp,Account account,CartItem item){ JsonResult result = new JsonResult(); String key = getKey(req, resp,account);//得到最終key ShoppingCart cacheCart = mergeCart(key , account);//根據key取得最終購物車對象 if(cacheCart!=null && cacheCart.getCartItems()!=null && cacheCart.getCartItems().size()>0){//⑴ // long count = cacheCart.getCartItems().stream().filter(it->it.getCode().equals(item.getCode())).count(); if(count == 1 ){//⑵ CartItem ci = cacheCart.getCartItems().stream().filter(it->it.getCode().equals(item.getCode())).findFirst().orElse(null); if (ci.getQuantity()>item.getQuantity()){//⑶ ci.setQuantity(ci.getQuantity()-item.getQuantity()); }else if(ci.getQuantity()<=item.getQuantity()){ cacheCart.getCartItems().remove(ci); } //1.滿足緩存購物車中必須有商品才能減購物車 //2.滿足緩存購物車中有該商品才能減購物車 //3.判斷此次要減數量是否大於緩存購物車中數量 進行移除還是數量相減操作 } HashOperations<String,String,ShoppingCart> vos = redisTemplate.opsForHash(); vos.put("CACHE_SHOPPINGCART",key, cacheCart); } result.setData(cacheCart); return result; } /** * 【場景:我加購了一雙40碼的鞋子到購物車 現在我想換成41碼的鞋子】 * 【例如:原商品code ABCDEFG40 -> ABCDEFG41】 * * @param req * @param resp * @param account * @param item 新購物商品 * @param oldItem 原購物商品 * @return */ public String updateCart(HttpServletRequest req, HttpServletResponse resp,Account account,CartItem item,CartItem oldItem){ //TODO 校驗商品信息是否合法 是否上架 庫存 最大購買數量.... if(false){ return null; } String key = getKey(req, resp,account); ShoppingCart cacheCart = mergeCart(key , account);//TODO 待探討 cacheCart.getCartItems().remove(item); cacheCart.getCartItems().remove(oldItem); cacheCart.getCartItems().add(oldItem); HashOperations<String,String,ShoppingCart> vos = redisTemplate.opsForHash(); vos.put("CACHE_SHOPPINGCART",key, cacheCart); return null; } }
controller調用:
package com.youxiu326.controller; import com.youxiu326.common.JsonResult; import com.youxiu326.entity.Account; import com.youxiu326.entity.CartItem; import com.youxiu326.entity.ShoppingCart; import com.youxiu326.service.ShoppingCartService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.util.WebUtils; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 購物車 Controller */ @Controller @RequestMapping("/shopping") public class ShoppingCartCtrl { private final Logger LOGGER = LoggerFactory.getLogger(this.getClass()); private Account account; @Autowired private ShoppingCartService service; /** * 進入首頁 * @return */ @GetMapping("/index") public String toIndex(HttpServletRequest req, HttpServletResponse resp, Model model){ account = (Account) req.getSession().getAttribute("account"); String key = service.getKey(req, resp, this.account); ShoppingCart cacheCart = service.mergeCart(key, this.account); model.addAttribute("cartItems",cacheCart.getCartItems()); return "index"; } @PostMapping("/add") @ResponseBody public JsonResult add(HttpServletRequest req, HttpServletResponse resp, CartItem cartItem){ account = (Account) req.getSession().getAttribute("account"); JsonResult result = service.addCart(req, resp, account, cartItem); return result; } @PostMapping("/remove") @ResponseBody public JsonResult remove(HttpServletRequest req, HttpServletResponse resp, CartItem cartItem){ account = (Account) req.getSession().getAttribute("account"); JsonResult result = service.removeCart(req, resp, account, cartItem); return result; } @PostMapping("/update") @ResponseBody public String update(HttpServletRequest req, HttpServletResponse resp){ account = (Account) req.getSession().getAttribute("account"); return ""; } }
演示效果:
項目源碼: https://github.com/youxiu326/sb_shopping_cart