购物车的原理以及实现
今天模拟京东的购物车实现原理完成了购物车模块的开发, 给大家分享下。
京东的购物车实现原理:在用户登录和不登录的状态下对购物车存入cookie还是持久化到redis中的实现。下面就来具体说次购物车的实现过程
两种情况:
用户登录,购物车存入redis中
用户未登录,购物车存入cookie中
比较两种方式的优缺点:
cookie:优点:数据保存在用户浏览器中,不占用服务端内存;用户体检效果好;代码实现简单
缺点:cookie的存储空间只有4k;更换设备时,购物车信息不能同步;cookie禁用,不提供保存
redis:优点:数据能够持久化;实现了购物车同步
缺点:增加了数据库的压力,速度慢
先介绍使用cookie存储购物车的实现思路
1、用户未登录状态下,用户添加购物车,首先从cookie中查询购物车中的商品列表
2、 判断cookie的商品列表中是否有要添加的商品信息
3、如果cookie中有该商品信息,将商品的数量相加
4、如果没有,根据商品的id值查询商品信息
5、将商品添加到购物车列表中
6、将购物车列表写入cookie中,设置cookie的过期时间
7、将cookie返回给客户端。
购物车的实现:
这里直接使用商品作为购物项对象,在页面中计算购物项的小计和购物车的总金额
package nyist.e3.pojo; import java.io.Serializable; import java.util.Date; public class TbItem implements Serializable{ private Long id; private String title; private String sellPoint; private Long price; private Integer num;//作为购物项购买的商品数量 private String barcode; private String image;//展示购物项中的图片 private Long cid; private Byte status; private Date created; private Date updated; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title == null ? null : title.trim(); } public String getSellPoint() { return sellPoint; } public void setSellPoint(String sellPoint) { this.sellPoint = sellPoint == null ? null : sellPoint.trim(); } 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 getBarcode() { return barcode; } public void setBarcode(String barcode) { this.barcode = barcode == null ? null : barcode.trim(); } public String getImage() { return image; } public void setImage(String image) { this.image = image == null ? null : image.trim(); } public Long getCid() { return cid; } public void setCid(Long cid) { this.cid = cid; } public Byte getStatus() { return status; } public void setStatus(Byte status) { this.status = status; } public Date getCreated() { return created; } public void setCreated(Date created) { this.created = created; } public Date getUpdated() { return updated; } public void setUpdated(Date updated) { this.updated = updated; } }
cookie中实现添加购物车的代码:
@Controller public class ShopCartController { @Autowired private TbItemService tbItemService; @Autowired private ShopCartService shopCartService; // 获取过期时间 @Value("${EXPIRE_KEY}") private Integer EXPIRE_KEY; @Value("${CART_COOKIE}") private String CART_COOKIE; /** * 需求:将商品加入购物车中未登录状态下,将购物超过添加到cookie中 * * 分析:1、从cookie中获取购物车信息 * 2、判断购物车中的商品,如果添加的商品存在,数量相加,不存在,根据商品id查询商品信息,添加到cookie中 * 3、将购物车列表信息写入cookie中 * * * @param itemId * @param num * @return */ @RequestMapping("/cart/add/{itemId}") public String addCart(@PathVariable Long itemId, @RequestParam(defaultValue = "1") Integer num, HttpServletRequest request, HttpServletResponse response) { // 1.获得购物车列表 List<TbItem> itemList = getCartItemList(request); // 用来判断商品是否存在的标志 boolean flag = false; // 2、循环遍列表中的商品信息 for (TbItem tbItem : itemList) { // 3、判断添加的商品是否存在 if (tbItem.getId() == itemId.longValue()) { // 4、添加的商品在cookie中存在,将数量相加 tbItem.setNum(tbItem.getNum() + num); // 重置标签 flag = true; // 跳出循环 break; } } if (!flag) { // cookie中没有添加的商品信息 // 通过商品id查询商品信息 TbItem item = tbItemService.getItemById(itemId); item.setNum(num); if (StringUtils.isNotBlank(item.getImage())) { // 取一张图片用于展示使用 item.setImage(item.getImage().split(",")[0]); } // 将商品添加购物车 itemList.add(item); } //将购物车写入cookie中 CookieUtils.setCookie(request, response, CART_COOKIE, JsonUtils.objectToJson(itemList),EXPIRE_KEY,true); return "cartSuccess"; } }
cookie中查询购物车列表:
思路:
1、根据cookie的name值直接取出商品列表信息
2、将购物车列表添加到model中,返回逻辑视图
private List<TbItem> getCartItemList(HttpServletRequest request) { // 使用utf-8,需要设置第三个参数为true String json = CookieUtils.getCookieValue(request, CART_COOKIE, true); if (StringUtils.isNotBlank(json)) { // 返回cookie中取出的数据集合 return JsonUtils.jsonToList(json, TbItem.class); } // 返回空集合对象 return new ArrayList<>(); } @RequestMapping("/cart/cart") public String getCartList(HttpServletRequest request, HttpServletResponse response, Model model) { // 从cookie中取出商品信息, List<TbItem> itemList = getCartItemList(request); // 将购物车信息返回给页面中 model.addAttribute("cartList", itemList); // 跳转逻辑视图 return "cart"; }
cookie中实现删除购物车中商品的功能:
1、接收页面传递的善品id值
2、从cookie中取出购物车列表,进行循环遍历,然后遍历的每一个商品信息和要删除的商品进行对比
3、如果存在就从购物车列表中将该商品移除
4、重新将购物车列表写入cookie中
5、将cookie信息响应给客户端
@RequestMapping("/cart/delete/{itemId}") public String deleteCartItem(@PathVariable Long itemId, HttpServletRequest request, HttpServletResponse response) { List<TbItem> list = getCartItemList(request); for (TbItem tbItem : list) { if (tbItem.getId() == itemId.longValue()) { list.remove(tbItem); break; } } // 删除成功后,将购物车列表写入cookie中 CookieUtils.setCookie(request, response, CART_COOKIE, JsonUtils.objectToJson(list), EXPIRE_KEY, true); // 删除成功后,重定向到购物车列表页面 return "redirect:/cart/cart.html"; }
cookie购物车的添加,查询,删除已经实现实现,更改方法和删除方法实现过程基本一样
登录状态下redis购物车的实现
实现redis购物车添加功能
思路:
1、从request域中取出登录用户的信息
2、使用redis存储购物车列表 使用redis中的hash数据类型 hash的key 使用登录用户id值,field的key使用商品的id值,将商品的信息作为field的value值
3、完成cookie存储购物车列表的功能
实现的代码:
@Override public E3Result addCart(Long userId, Long itemId, Integer num) { try { // 从redis中取出购物车,判断是否已经有购物项 Boolean hexists = jedisClient.hexists(CART_REDIS_KEY_PRE + ":" + userId + "", itemId + ""); if (hexists) { // 表示购物车中已经有该商品,只需要将该商品的数量相加即可 String hget = jedisClient.hget(CART_REDIS_KEY_PRE + ":" + userId + "", itemId + ""); // 将数量相加 TbItem item = JsonUtils.jsonToPojo(hget, TbItem.class); item.setNum(item.getNum() + num); // 将商品重新放入购物车中 jedisClient.hset(CART_REDIS_KEY_PRE + ":" + userId + "", itemId + "", JsonUtils.objectToJson(item)); return E3Result.ok(); } // 表示购物车中没有要添加的商品信息 // 根据商品的id查询商品的信息 TbItem item = itemMapper.selectByPrimaryKey(itemId); item.setNum(num); if (StringUtils.isNotBlank(item.getImage())) { item.setImage(item.getImage().split(",")[0]); } // 将商品信息存入购物车中 jedisClient.hset(CART_REDIS_KEY_PRE + ":" + userId + "", itemId + "", JsonUtils.objectToJson(item)); return E3Result.ok(); } catch (Exception e) { e.printStackTrace(); } return E3Result.build(400, "商品添加购物车失败"); }
展示登录状态下的购物车列表:需要将cookie中的购物车和redis中的购物车整合
1、从cookie中取出购物车列表对象
2、从redis中取出购物车对象
3、将cookie中的购物车列表和redis中的购物车列表整合(取出cookie中的购物车列表,然后添加到redis购物车中即可)
5、最终展示的结果以redis中的购物车为主
/** * cookie中的购物车和redis中的购物车进行整合 */ @Override public E3Result mergeCart(Long userId, List<TbItem> itemList) { for (TbItem tbItem : itemList) { // 只需要调用登录状态下添加购物车业务处理逻辑即可 addCart(userId, tbItem.getId(), tbItem.getNum()); } return E3Result.ok(); }
redis购物车中删除购物项
将用户的id值和商品的id值分别作为hahs的key和field的key,调用redis中的hdel(String key,String...field)即可完成删除功能
/** * 删除购物车 * * @return * */ @Override public E3Result deleteCartItem(Long id, Long itemId) { Long hdel = jedisClient.hdel(CART_REDIS_KEY_PRE + ":" + id + "", itemId + ""); System.out.println("删除购物车购物项为"+hdel); return E3Result.ok(); }
redis购物车中更新购买商品的数量
/** * 更新购物车中商品的数量 */ @Override public E3Result updateRedisNum(Long id, Long itemId, Integer num) { // 取出需要更改数量的商品信息 String hget = jedisClient.hget(CART_REDIS_KEY_PRE + ":" + id + "", itemId + ""); // 将取出的json数据转换为商品对象,然后更新数量 TbItem item = JsonUtils.jsonToPojo(hget, TbItem.class); item.setNum(num); // 更新成功后,将数据写到redis购物车中 jedisClient.hset(CART_REDIS_KEY_PRE + ":" + id + "", itemId + "", JsonUtils.objectToJson(item)); return E3Result.ok(); }
当用户点击去结算时:跳转到订单确认页面
1、生成订单详情
2、配送地址信息
3、选择支付方式
在确认订单之前, 应该判断用户是否是登录装态,可以使用拦截器实现
1、自定义拦截器实现HandlerInteceptor接口
2、从cookie中去token消息(登录认证的令牌)
3、判断token的值是否为空,如果为空,就跳转到用户登录页面完成登录,同时需要将当前地址栏的url作为参数传递(在登录的业务逻辑中,接收该url,完成登录后,跳转会该页面)
4、如果token不为空,根据token查询用户信息,然后将用户信息写入request域中,拦截器执行放行操作
5、此时获取到的购物车列表是从redis中读出的和cookie整合的最新的购物车。
拦截器的实现过程:
public class LoginInterceptor implements HandlerInterceptor { @Value("${TT_TOKEN}") private String TT_TOKEN; @Value("${SSO_LOGIN_URL}") private String SSO_LOGIN_URL; @Autowired private UserService userService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //执行Handler之前执行此方法 // a)从cookie中取token。 String token = CookieUtils.getCookieValue(request, TT_TOKEN); if (StringUtils.isBlank(token)) { //取当前请求的url String url = request.getRequestURL().toString(); // b)没有token,需要跳转到登录页面。 response.sendRedirect(SSO_LOGIN_URL + "?redirectUrl=" + url); //拦截 return false; } // c)有token。调用sso系统的服务,根据token查询用户信息。 e3Result result = userService.getUserByToken(token); if (result.getStatus() != 200) { // d)如果查不到用户信息。用户登录已经过期。需要跳转到登录页面。 //取当前请求的url String url = request.getRequestURL().toString(); // b)没有token,需要跳转到登录页面。 response.sendRedirect(SSO_LOGIN_URL + "?redirectUrl=" + url); //拦截 return false; } // e)查询到用户信息。放行。 return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { // 执行Handler之后返回ModelAndView之前 } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 返回ModelAndView之后,执行。异常处理。 } }
@Override public E3Result getToken(String token) { try { // 从redis中取值 String json = jedisClient.get("USER_INFO:" + token); if (StringUtils.isBlank(json)) { // json为空,表示已经过期 return E3Result.build(400, "session已经过期,请重新登录"); } //将json对象转化为pojo对象 TbUser user = JsonUtils.jsonToPojo(json, TbUser.class); //重新设置用户登录信息的过期时间 jedisClient.expire("USER_INFO:" + token, 1800); //将获取的user信息使用E3Result包装后返回 return E3Result.ok(user); } catch (Exception e) { e.printStackTrace(); } return null; }
拦截器定义好之后,需要在springmvc中配置
<mvc:interceptors> <mvc:interceptor> <!-- 拦截所有请求 --> <mvc:mapping path="/**" /> <!-- 注册自定义拦截器 --> <bean class="nyist.e3.interceptor.LoginInterceptor"></bean> </mvc:interceptor> </mvc:interceptors>
在登录页面接收url,实现sso系统的回调 接收的redirectUrl即为拦截中请求登录页面传递的参数
至此:购物车模块的功能基本实现,错误的地方希望大家多多指正。