基于redis实现未登录购物车
- 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