0%

购物车解决方案

购物车需求分析与解决方案

需求分析

​ 用户在商品详细页点击加入购物车,提交商品SKU编号和购买数量,添加到购物车。购物车展示页面如下:

实现思路

​ 购物车数据的存储结构如下(总购物车包含若干商家购物车):

​ 当用户在未登录的情况下,将此购物车存入cookies , 在用户登陆的情况下,将购物车数据存入redis 。如果用户登陆时,cookies中存在购物车,需要将cookies的购物车合并到redis中存储.

购物车实体类VO

1
2
3
4
5
6
public class Cart implements Serializable{
private String sellerId;//商家ID
private String sellerName;//商家名称
private List<TbOrderItem> orderItemList;//购物车明细
//getter and setter ......
}

​ 这个类是对每个商家的购物车进行的封装

Cookie存储购物车

需求分析

​ 使用cookie存储购物车数据。服务层负责逻辑,控制层负责读写cookie

服务接口层

1
2
3
public interface CartService {
public List<Cart> addGoodsToCartList(List<Cart> cartList,Long itemId,Integer num );
}

服务实现层

实现思路:

  • 1.根据商品SKU ID查询SKU商品信息
  • 2.获取商家ID
  • 3.根据商家ID判断购物车列表中是否存在该商家的购物车
  • 4.如果购物车列表中不存在该商家的购物车
    • 4.1 新建购物车对象
    • 4.2 将新建的购物车对象添加到购物车列表
  • 5.如果购物车列表中存在该商家的购物车,查询购物车明细列表中是否存在该商品
    • 5.1. 如果没有,新增购物车明细
    • 5.2. 如果有,在原购物车明细上添加数量,更改金额

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
//增加明细到购物车中
@Override
public List<Cart> addItem2Cart(Long itemId, Integer num, List<Cart> cartList) {

TbItem item = itemMapper.selectByPrimaryKey(itemId);
if (item == null) {
throw new RuntimeException("该商品不存在:" + itemId);
}
if ("1".equals(item.getStatus()) == false) {
throw new RuntimeException("商品状态不正常!");
}

//若购物车有该商品所属的商家
Cart cart = searchCartFromCartListBySellerId(item.getSellerId(), cartList);
if (cart != null) {
//1、若商家购物车已有该商品
TbOrderItem orderItem = searchOrderItemFromCart(item, cart);
if (orderItem != null) {
//若已有该商品,则累加数量并重新计算小计
orderItem.setNum(orderItem.getNum() + num);
orderItem.setTotalFee(new BigDecimal( orderItem.getPrice().doubleValue() * orderItem.getNum()));
//若数量为负,则为拣出购物车,若全部拣出,则从该购物车中删除该条目
if (orderItem.getNum() <= 0) {
cart.getOrderItemList().remove(orderItem);
}
//若该商家购物车中没有条目了,则从总购物车中删除商家购物车
if (cart.getOrderItemList().size() == 0) {
cartList.remove(cart);
}
//操作完成
return cartList;
}

//2、若商家购物车没有该商品,则新建条目并添加到该购物车中
cart.getOrderItemList().add(createOrderItem(item, num));
//操作完毕
return cartList;
}

//3、若购物车没有该商品商家,则新建商家购物车并添加条目,最后将商家购物车添加到总购物车
cart = new Cart();
cart.setSellerId(item.getSellerId());
cart.setSellerName(sellerMapper.selectByPrimaryKey(cart.getSellerId()).getName());
List<TbOrderItem> orderItemList = new ArrayList<TbOrderItem>();
orderItemList.add(createOrderItem(item, num));
cart.setOrderItemList(orderItemList);
cartList.add(cart);
//操作完毕
return cartList;
}

//根据SKU和数量创建明细
private TbOrderItem createOrderItem(TbItem item,Integer num) {
if(num <= 0){
throw new RuntimeException("数量非法!");
}
TbOrderItem orderItem = new TbOrderItem();
orderItem.setItemId(item.getId());
orderItem.setGoodsId(item.getGoodsId());
orderItem.setSellerId(item.getSellerId());
orderItem.setPicPath(item.getImage());
orderItem.setTitle(item.getTitle());
orderItem.setPrice(item.getPrice());
orderItem.setNum(num);
orderItem.setTotalFee(new BigDecimal( item.getPrice().doubleValue() * orderItem.getNum().doubleValue()+""));
return orderItem;
}

//从商家购物车中根据SKU ID查询明细
private TbOrderItem searchOrderItemFromCart(TbItem item, Cart cart) {
if (cart != null) {
for (TbOrderItem orderItem : cart.getOrderItemList()) {
if (orderItem.getItemId().equals(item.getId())) {
return orderItem;
}
}
}
return null;
}

//从总购物车中根据商家ID查商家购物车
private Cart searchCartFromCartListBySellerId(String sellerId, List<Cart> cartList) {
for (Cart cart : cartList) {
if (cart.getSellerId().equals(sellerId)) {
return cart;
}
}
return null;
}

控制层

  • 提供增加/拣出SKU到购物车和获取购物车数据的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Reference
private CartService cartService;

public static final int ONE_DAY=60*60*24;

@Autowired
private HttpServletRequest request;
@Autowired
private HttpServletResponse response;

public static final String UTF_8 = "UTF-8";
public static final String CART_COOKIE_NAME = "cart";

@RequestMapping("/addGoodsToCartList")
public Result addGoodsToCartList(Long itemId,@RequestParam(defaultValue = "1") Integer num) {
try {

//调用服务添加商品到购物车
List<Cart> cartList = cartService.addItem2Cart(itemId, num, getCartListFromCookie());
saveCartList2Cookie(cartList);
return Result.success();
} catch (Exception e) {
e.printStackTrace();
return Result.fail();
}
}

public static final String JSON_ARRAY_PREFIX = "[";

@RequestMapping("/findCartList")
public List<Cart> findCartList() {
//从cookie中取cartList
String cartListJson = CookieUtil.getCookieValue(request, CART_COOKIE_NAME, UTF_8);
if (StringUtils.isEmpty(cartListJson) || cartListJson.startsWith(JSON_ARRAY_PREFIX)==false) {
cartListJson = "[]";
}
return JSON.parseArray(cartListJson, Cart.class);
}

public void saveCartList2Cookie(List<Cart> cartList) {
//将购物车写入cookie
CookieUtil.setCookie(request, response, CART_COOKIE_NAME,JSON.toJSONString(cartList),ONE_DAY, UTF_8);
}

购物车前端代码

需求分析

​ 实现购物车页面的展示与相关操作

​ 可以实现购物车列表、数量的增减与移除以及更新小计、合计

购物车列表

前端服务层

  • cartService
1
2
3
4
5
6
app.service('cartService',function($http){
//购物车列表
this.findCartList=function(){
return $http.get('cart/findCartList.do');
}
});
  • cartController.js
1
2
3
4
5
6
7
8
9
10
app.controller('cartController',function($scope,cartService){
//查询购物车列表
$scope.findCartList=function(){
cartService.findCartList().success(
function(response){
$scope.cartList=response;
}
);
}
});
  • 页面加载时请求显示购物车列表
1
<body ng-app="myApp" ng-controller="cartController" ng-init="findCartList()">
  • 遍历cartList
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<div class="cart-item-list" ng-repeat="cart in cartList">
<div class="cart-shop">
<input type="checkbox" name="" id="" value="" />
<span class="shopname self">{{cart.sellerName}}【商家ID:{{cart.sellerId}}】</span>
</div>
<div class="cart-body">
<div class="cart-list" ng-repeat="orderItem in cart.orderItemList">
<ul class="goods-list yui3-g">
<li class="yui3-u-1-24">
<input type="checkbox" name="" id="" value="" />
</li>
<li class="yui3-u-11-24">
<div class="good-item">
<div class="item-img"><img src="{{orderItem.picPath}}" /></div>
<div class="item-msg">
{{orderItem.title}}
</div>
</div>
</li>
<li class="yui3-u-1-8"><span class="price">{{orderItem.price.toFixed(2)}}</span></li>
<li class="yui3-u-1-8">
<a href="javascript:void(0)" class="increment mins">-</a>
<input autocomplete="off" type="text" ng-model="orderItem.num" minnum="1" class="itxt" />
<a href="javascript:void(0)" class="increment plus">+</a>
</li>
<li class="yui3-u-1-8"><span class="sum">{{orderItem.totalFee.toFixed(2)}}</span></li>
<li class="yui3-u-1-8">
<a href="#none">删除</a><br />
<a href="#none">移到我的关注</a>
</li>
</ul>
</div>
</div>
</div>

购物车数量增减与移除

  • cartService.js
1
2
3
4
//添加商品到购物车
this.addGoodsToCartList=function(itemId,num){
return $http.get('cart/addGoodsToCartList.do?itemId='+itemId+'&num='+num);
}
  • cartController.js
1
2
3
4
5
6
7
8
9
10
11
12
//添加商品到购物车
$scope.addGoodsToCartList=function(itemId,num){
cartService.addGoodsToCartList(itemId,num).success(
function(response){
if(response.success){
$scope.findCartList();//刷新列表
}else{
alert(response.message);//弹出错误提示
}
}
);
}
  • 页面调用
1
2
3
4
5
6
7
8
9
10
11
<li class="yui3-u-1-8">
<a href="javascript:void(0)" ng-click="addGoodsToCartList(orderItem.itemId,-1)" class="increment mins">
-
</a>

<input autocomplete="off" type="text" ng-model="orderItem.num" minnum="1" class="itxt" />

<a href="javascript:void(0)" ng-click="addGoodsToCartList(orderItem.itemId,1)" class="increment plus">
+
</a>
</li>

实现删除功能

1
2
3
4
<li class="yui3-u-1-8">
<a href="#none" ng-click="addGoodsToCartList(orderItem.itemId,-orderItem.num)">删除</a><br />
<a href="#none">移到我的关注</a>
</li>

更新合计

  • 修改cartService.js
1
2
3
4
5
6
7
8
9
10
11
12
13
//求合计
this.sum=function(cartList){
var totalValue={totalNum:0, totalMoney:0.00 };//合计实体
for(var i=0;i<cartList.length;i++){
var cart=cartList[i];
for(var j=0;j<cart.orderItemList.length;j++){
var orderItem=cart.orderItemList[j];//购物车明细
totalValue.totalNum+=orderItem.num;
totalValue.totalMoney+= orderItem.totalFee;
}
}
return totalValue;
}
  • cartController
1
2
3
4
5
6
7
8
9
//查询购物车列表
$scope.findCartList=function(){
cartService.findCartList().success(
function(response){
$scope.cartList=response;
$scope.totalValue=cartService.sum($scope.cartList);//求合计数
}
);
}
  • 页面
1
2
3
4
5
6
<div class="chosed">已选择<span>{{totalValue.totalNum}}</span>件商品</div>
<div class="sumprice">
<span>
<em>总价(不含运费) :</em><i class="summoney">¥{{totalValue.totalMoney}}</i>
</span>
</div>

Redis存储购物车

获取当前登录人账号

配置文件

  • 修改spring-security.xml (删一行,加一行):
1
2
3
4
5
6
7
8
9
10
11
12
13
<!--
删除此行,访问/cart/*.do时相当于绕道走,没有进过security,获取登录名会抛出空指针异常
<http pattern="/cart/*.do" security="none"></http>
-->

<http use-expressions="false" entry-point-ref="casProcessingFilterEntryPoint">
<!-- 增加此行,访问/cart/*.do时相当于光明正大走,经过了security,security发一张绿卡,获取登录名为anonymousUser -->
<intercept-url pattern="/cart/*.do" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<intercept-url pattern="/**" access="ROLE_USER"/>
<custom-filter position="CAS_FILTER" ref="casAuthenticationFilter" />
<custom-filter ref="requestSingleLogoutFilter" before="LOGOUT_FILTER"/>
<custom-filter ref="singleLogoutFilter" before="CAS_FILTER"/>
</http>

access="IS_AUTHENTICATED_ANONYMOUSLY" 用于设置资源可以在不登陆时可以访问。

此配置与 security=”none”的区别在于当用户未登陆时获取登陆人账号的值(SecurityContext.getContext().getAuthtication().getName())为anonymousUser ,而security=”none”的话,无论是否登陆都不能获取登录人账号的值。

代码实现

pinyougou-cart-webfindCartList和addGoodsToCartList方法中,获取用户名:

1
2
//得到登陆人账号,判断当前是否有人登陆
String username = SecurityContextHolder.getContext().getAuthentication().getName();

测试:当用户未登陆时,username的值为anonymousUser,若按之前的配置则为null

远程购物车存取

服务接口层

pinyougou-cart-interfaceCartService.java定义方法

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 从redis中查询购物车
* @param username
* @return
*/
public List<Cart> findCartListFromRedis(String username);

/**
* 将购物车保存到redis
* @param username
* @param cartList
*/
public void saveCartListToRedis(String username,List<Cart> cartList);

服务实现层

pinyougou-cart-serviceCartServiceImpl.java实现方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Autowired
private RedisTemplate redisTemplate;
@Override
public List<Cart> findCartListFromRedis(String username) {
System.out.println("从redis中提取购物车数据....."+username);
List<Cart> cartList = (List<Cart>) redisTemplate.boundHashOps("cartList").get(username);
if(cartList==null){
cartList=new ArrayList();
}
return cartList;
}
@Override
public void saveCartListToRedis(String username, List<Cart> cartList) {
System.out.println("向redis存入购物车数据....."+username);
redisTemplate.boundHashOps("cartList").put(username, cartList);
}

控制层

修改CartController.javafindCartList方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 购物车列表
* @param request
* @return
*/
@RequestMapping("/findCartList")
public List<Cart> findCartList(){
String username = SecurityContextHolder.getContext().getAuthentication().getName();
if(username.equals("anonymousUser")){//如果未登录
//读取本地购物车//
..........
return cartList_cookie;
}else{//如果已登录
List<Cart> cartList_redis =cartService.findCartListFromRedis(username);//从redis中提取
return cartList_redis;
}
}

修改addGoodsToCartList方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 添加商品到购物车
* @param request
* @param response
* @param itemId
* @param num
* @return
*/
@RequestMapping("/addGoodsToCartList")
public Result addGoodsToCartList(Long itemId,Integer num){
String username = SecurityContextHolder.getContext().getAuthentication().getName();
System.out.println("当前登录用户:"+username);
try {
List<Cart> cartList =findCartList();//获取购物车列表
cartList = cartService.addGoodsToCartList(cartList, itemId, num);
if(username.equals("anonymousUser")){ //如果是未登录,保存到cookie
util.CookieUtil.setCookie(request, response, "cartList", JSON.toJSONString(cartList),3600*24 ,"UTF-8");
System.out.println("向cookie存入数据");
}else{//如果是已登录,保存到redis
cartService.saveCartListToRedis(username, cartList);
}
return new Result(true, "添加成功");
} catch (RuntimeException e) {
e.printStackTrace();
return new Result(false, e.getMessage());
}catch (Exception e) {
e.printStackTrace();
return new Result(false, "添加失败");
}
}

为避免调用远程服务超时,我们可以将过期时间改为6秒(默认为1秒)

1
2
@Reference(timeout=6000)
private CartService cartService;

跳板页

(1)创建跳板页:pinyougou-cart-web 工程新建login.html ,页面添加脚本

1
2
3
<script type="text/javascript">
location.href="cart.html";
</script>

(2)购物车页面链接到跳板页

1
<a href="login.html">登录</a>

这里用了个小技巧:当用户点击登录时,由于login.html在security的拦截规则之中,因此跳到cas系统登录页,当登录之后回调调往login.html,而login.html又跳往cart.html

购物车合并

服务接口层

pinyougou-cart-interface工程的CartService.java定义方法

1
2
3
4
5
6
7
/**
* 合并购物车
* @param cartList1
* @param cartList2
* @return
*/
public List<Cart> mergeCartList(List<Cart> cartList1,List<Cart> cartList2);

服务实现层

pinyougou-cart-service工程CartServiceImpl.java实现方法

1
2
3
4
5
6
7
8
9
public List<Cart> mergeCartList(List<Cart> cartList1, List<Cart> cartList2) {
System.out.println("合并购物车");
for(Cart cart: cartList2){
for(TbOrderItem orderItem:cart.getOrderItemList()){
cartList1= addGoodsToCartList(cartList1,orderItem.getItemId(),orderItem.getNum());
}
}
return cartList1;
}

在何时合并?当用户登录后势必会跳回购物车列表页,因此在查询list时合并一次即可。细节:合并前判断cookie中是否存在购物车数据,有则合并并清空cookie购物车,无则跳过。

控制层

修改pinyougou-cart-web工程CartController类的findCartList方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RequestMapping("/findCartList")
public List<Cart> findCartList(){
String username = SecurityContextHolder.getContext().getAuthentication().getName();
String cartListString = util.CookieUtil.getCookieValue(request, "cartList", "UTF-8");
if(cartListString==null || cartListString.equals("")){
cartListString="[]";
}
List<Cart> cartList_cookie = JSON.parseArray(cartListString, Cart.class);
if(username.equals("anonymousUser")){//如果未登录
return cartList_cookie;
}else{
List<Cart> cartList_redis =cartService.findCartListFromRedis(username);//从redis中提取
if(cartList_cookie.size()>0){//如果本地存在购物车
//合并购物车
cartList_redis=cartService.mergeCartList(cartList_redis, cartList_cookie);
//清除本地cookie的数据
util.CookieUtil.deleteCookie(request, response, "cartList");
//将合并后的数据存入redis
cartService.saveCartListToRedis(username, cartList_redis);
}
return cartList_redis;
}
}
鼓励一下~