畅购商城之购物车模块

1、需求

  • 用户已登录:购物车保存在服务器端的redis中
  • 用户未登录形态:购物车信息保存在浏览器端的localStroage中。

2、环境

  • pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>changgou3_parent</artifactId>
        <groupId>com.czxy</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>changgou3_cart_service</artifactId>

    <dependencies>
        <!--pojo-->
        <dependency>
            <groupId>com.czxy</groupId>
            <artifactId>changgou3_pojo</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
        <!--json转换-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
        <dependency>
            <groupId>com.czxy</groupId>
            <artifactId>changgou3_common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.czxy</groupId>
            <artifactId>changgou3_common_auth</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>
  • yml文件
server:
  port: 8095
spring:
  application:
    name: gccartservice
  redis:
    host: 127.0.0.1
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
    registry-fetch-interval-seconds: 5
  instance:
    prefer-ip-address: true
    ip-address: 127.0.0.1
    instance-id: ${eureka.instance.ip-address}.${server.port}
    lease-renewal-interval-in-seconds: 3
    lease-expiration-duration-in-seconds: 10

sc:
  jwt:
    secret: sc@Login(Auth}*^31)&czxy% # 登录校验的密钥
    pubKeyPath: D:/rsa/rsa.pub # 公钥地址
    priKeyPath: D:/rsa/rsa.pri # 私钥地址
    expire: 360 # 过期时间,单位分钟

  • 拷贝JWT配置

package com.czxy.config;

import com.czxy.utils.RasUtils;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.io.File;
import java.security.PrivateKey;
import java.security.PublicKey;

/**
 * @author 庭前云落
 * @Date 2020/4/17 13:02
 * @description
 */
@Data
@ConfigurationProperties(prefix = "sc.jwt")
@Component
public class JwtProperties {
    private String secret; // 密钥

    private String pubKeyPath;// 公钥

    private String priKeyPath;// 私钥

    private int expire;// token过期时间

    private PublicKey publicKey; // 公钥

    private PrivateKey privateKey; // 私钥

    private static final Logger logger = LoggerFactory.getLogger(JwtProperties.class);

    @PostConstruct
    public void init(){
        try {
            File pubFile = new File(this.pubKeyPath);
            File priFile = new File(this.priKeyPath);
            if( !pubFile.exists() || !priFile.exists()){
                RasUtils.generateKey( this.pubKeyPath ,this.priKeyPath , this.secret);
            }
            this.publicKey = RasUtils.getPublicKey( this.pubKeyPath );
            this.privateKey = RasUtils.getPrivateKey( this.priKeyPath );
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
  • 启动类
package com.czxy;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * @author 庭前云落
 * @Date 2020/4/17 13:01
 * @description
 */
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class CartServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(CartServiceApplication.class,args);
    }
}

3、加入购物车

3.1、流程分析

  • 浏览器端形态:

  • 服务器端形态:

3.2、JavaBean

  • 购物车无论在浏览器的localStorage中,还是在服务端redis中,我们都可以理解成在内存中。没有在数据库中保存,数据库没有对应的表。
  • 需要2个对象,存放购物车完整信息
    • 购物项CartItem:某一个商品的购买情况。例如:iponex买了333件
    • 购物车Cart:整个购买情况。例如:买了2种商品,一共 xxx 元

购物项CartItem
package com.czxy.cart;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;

import java.util.Map;

/**
 * @author 庭前云落
 * @Date 2020/4/17 13:09
 * @description
 */
@Data
public class CartItem {
    private Integer skuid;        //sku(商品)id
    private Integer spuid;        //spu(小分类)id
    @JsonProperty("goods_name")
    private String goodsName;     //商品名称
    private Double price;         //价格
    private Integer count;        //购买数量
    private Boolean checked;      //是否选中 
    private String midlogo;       //图标
    @JsonProperty("spec_info_id_txt")
    private Map<String,String> specInfoIdTxt;   //规格信息
}

Cart实现
package com.czxy.cart;

import lombok.Data;

import java.util.HashMap;
import java.util.Map;

/**
 * @author 庭前云落
 * @Date 2020/4/17 13:11
 * @description 购物车:一个购物车存放多个购物项
 */
@Data
public class Cart {
    //存放多个购物项,Map适合快速查询
    private Map<Integer, CartItem> data = new HashMap<>();
    //存放总价
    private Double total;

    //覆盖total使用@Data默认getter方法
    public Double getTotal() {
        //总价:所有小计的和
        //小计:单价*数量
        double sum = 0.0;
        for (CartItem cartItem : data.values()) {
            //只统计勾选的价格
            if (cartItem.getChecked()) {
                sum += (cartItem.getPrice() * cartItem.getCount());
            }
        }
        return sum;
    }

    //将物品添加到购物车
    public void addCart(CartItem cartItem) {
        //从data中获得购物车
        CartItem temp = data.get(cartItem.getSkuid());
        //如果购物车中没有当前物品直接添加
        if (temp == null) {
            data.put(cartItem.getSkuid(), cartItem);
        } else {
            //如果购物车中有当前购物项,将更新购物车数量
            temp.setCount(cartItem.getCount() + temp.getCount());
        }
    }


    //更新数据
    public void updateCart(Integer skuid, Integer count, Boolean checked) {
        //如果skuid存在,将更新数据
        CartItem temp = data.get(skuid);
        if (temp != null) {
            temp.setCount(count);
            temp.setChecked(checked);
        }
    }

    //从购物车中移除
    public void deleteCart(Integer skuid) {
        data.remove(skuid);
    }
}

3.3、后端实现

3.3.1、添加购物车

  • 编写SKuFeign,用于远程调用"查询sku的详情"
package com.czxy.client;

import com.czxy.pojo.OneSkuResult;
import com.czxy.vo.BaseResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 * @author 庭前云落
 * @Date 2020/4/17 13:12
 * @description
 */
@FeignClient(value="cgwebservice",path = "/sku")
public interface SkuClient {
    @GetMapping("/goods/{skuid}")
    public BaseResult<OneSkuResult> findSkuById(@PathVariable("skuid") Integer skuid);
}

  • 编写CartVo,用于封装请求参数
package com.czxy.vo;

import lombok.Data;

/**
 * @author 庭前云落
 * @Date 2020/4/17 13:13
 * @description
 */
@Data
public class CartVo {
    private Integer skuid ;     //"SKUID",
    private Integer count;      //"购买数量"
    private Boolean checked;    //"购买数量"
}

  • Controller
package com.czxy.controller;

import com.czxy.cart.Cart;
import com.czxy.config.JwtProperties;
import com.czxy.pojo.User;
import com.czxy.service.CartService;
import com.czxy.utils.JwtUtils;
import com.czxy.vo.BaseResult;
import com.czxy.vo.CartVo;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.List;

/**
 * @author 庭前云落
 * @Date 2020/4/17 13:12
 * @description
 */
@RestController
@RequestMapping("/carts")
public class CartController {
    @Resource
    private CartService cartService;

    @Resource
    private HttpServletRequest request;

    @Resource
    private JwtProperties jwtProperties;

    @PostMapping
    public BaseResult addCart(@RequestBody CartVo cartVo) {

        //1 获得用户信息
        // 1.1 获得token
        String token = request.getHeader("Authorization");
        // 1.2 解析token
        User loginUser = null;
        try {
            loginUser = JwtUtils.getObjectFromToken(token, jwtProperties.getPublicKey(), User.class);
        } catch (Exception e) {
            return BaseResult.error("token失效或未登录");
        }

        //2 添加操作
        cartService.addCart(loginUser, cartVo);

        //3 提示
        return BaseResult.ok("添加成功");
    }

    @GetMapping
    public BaseResult findCartList() {
        String token = request.getHeader("Authorization");
        User loginUser = null;
        try {
            loginUser = JwtUtils.getObjectFromToken(token, jwtProperties.getPublicKey(), User.class);
        } catch (Exception e) {
            return BaseResult.error("token失效或未登录");
        }
        Cart cart = cartService.findCartList(loginUser);
        System.out.println(cart);
        return BaseResult.ok("查询成功", cart.getData().values());
    }
}

  • Service
package com.czxy.service.Impl;

import com.alibaba.fastjson.JSON;
import com.czxy.cart.Cart;
import com.czxy.cart.CartItem;
import com.czxy.client.SkuClient;
import com.czxy.pojo.OneSkuResult;
import com.czxy.pojo.User;
import com.czxy.service.CartService;
import com.czxy.vo.BaseResult;
import com.czxy.vo.CartVo;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author 庭前云落
 * @Date 2020/4/17 13:12
 * @description
 */
@Service
public class CartServiceImpl implements CartService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private SkuClient skuClient;

    /**
     * 给指定的用户,添加商品
     *
     * @param user
     * @param cartVo
     */
    public void addCart(User user, CartVo cartVo) {
        //查询sku
        BaseResult<OneSkuResult> baseResult = skuClient.findSkuById(cartVo.getSkuid());
        OneSkuResult oneSkuResult = baseResult.getData();

        //封装CarItem
        CartItem cartItem = new CartItem();
        cartItem.setSkuid( oneSkuResult.getSkuid() );
        cartItem.setSpuid( oneSkuResult.getSpuid() );
        cartItem.setGoodsName( oneSkuResult.getGoodsName() );
        cartItem.setPrice( oneSkuResult.getPrice() );
        cartItem.setCount( cartVo.getCount() );
        cartItem.setChecked( cartVo.getChecked() );
        cartItem.setMidlogo( oneSkuResult.getLogo().get("biglogo") );
        cartItem.setSpecInfoIdTxt( JSON.parseObject(oneSkuResult.getSpecInfo().get("id_txt"),Map.class));

        //从redis获得购物车,如果不存在需要创建一个新的,如果存在(字符串-->java对象)
        String cartJSON = stringRedisTemplate.opsForValue().get("cart" + user.getId());
        Cart cart = null;
        if(cartJSON != null){
            //将字符串转换成java对象
            cart = JSON.parseObject(cartJSON, Cart.class);
        } else {
            //创建新购物车
            cart = new Cart();
        }

        //添加购物车
        cart.addCart( cartItem );

        //保存reids
        cartJSON = JSON.toJSONString(cart);
        stringRedisTemplate.opsForValue().set("cart" + user.getId() , cartJSON);
    }


    //查询当前用户购物车
    public Cart findCartList(User user) {

        String cartString = this.stringRedisTemplate.opsForValue().get("cart" + user.getId());

        return JSON.parseObject(cartString, Cart.class);
    }
}

3.3.2、更新/删除购物车

  • Controller,必须保证用户登录状态
    @PutMapping
    public BaseResult updateCart(@RequestBody List<CartVo> cartVoList) {
        String token = request.getHeader("Authorization");
        User loginUser = null;
        try {
            loginUser = JwtUtils.getObjectFromToken(token, jwtProperties.getPublicKey(), User.class);
        } catch (Exception e) {
            return BaseResult.error("token失效或未登录");
        }

        try {
            //更新
            cartService.updateCart(loginUser, cartVoList);
            return BaseResult.ok("成功");
        } catch (Exception e) {
            return BaseResult.error(e.getMessage());
        }
    }
  • service,通过redis查询指定用户购物车
    • 如果提交的数据不在redis中,将删除
    • 如果提交的数据在redis中,将更新数据
    /**
     * 更新操作:如果数据存在 --》修改,如果数据不存在删除
     * @param user
     */
    public void updateCart(User user, List<CartVo> cartVoList) {
        //从redis获得购物车
        String cartStr = stringRedisTemplate.opsForValue().get( "cart" + user.getId());
        Cart cart = JSON.parseObject(cartStr, Cart.class);
        if (cart == null) {
            throw new RuntimeException("购物车不存在");
        }
        //处理请求数据,方便进行比那里,采用mapper进行处理
        HashMap<Integer, CartVo> map = new HashMap<>();
        for (CartVo cartVo : cartVoList) {
            map.put(cartVo.getSkuid(), cartVo);
        }
        
        //请求数据和reids中存放进行对比
        for (CartItem cartItem : cart.getData().values()) {
            //匹配成功:更新数据;不成功:删除数据
            CartVo cartVo = map.get(cartItem.getSkuid());
            if (cartVo != null) {
                //更新
                cart.updateCart(cartVo.getSkuid(), cartVo.getCount(), cartVo.getChecked());
            } else {
                //删除 
                cart.deleteCart(cartItem.getSkuid());
            }
        }
        //更新购物车
        stringRedisTemplate.opsForValue().set("cart" + user.getId(), JSON.toJSONString(cart));
    }

3.4、前端实现

3.4.1、总金额
  • 总金额需要实时更新,使用"计算属性",直接求和

  computed: {
    totalPrice () {
      //求和
      let sum = 0
      //遍历,求所有小计的和
      this.cart.forEach(item=>{
        sum += (item.price * item.count)
      })

      return sum
    }
  },
3.4.2、加/减操作

  methods: {
    minus(item) {
      //减操作
      if(item.count > 1){
        item.count --
      }
    },
    plus(item) {
      //加操作
      item.count++
    }
  },
  • 编写watch,监控cart,将修改后的数据提交给服务端

    • watch基本用法
      watch: {
        cart : {
          handler(newCart , oldCart ) {
            console.info(newCart)
          },
          immediate: false, //true代表如果在 watch 里声明了之后,就会立即先去执行里面的handler方法
          deep: true      //深度监听,常用于对象下面属性的改变
        }
      },
    
    • 实际代码
      watch: {
        //深度监听
        cart: {
          async handler(newCart, oldCart) {
            //根据登录情况,更新购物车
            let token = sessionStorage.getItem("token");
            if (token) {
              //登录
              let { data } = await this.$request.updateCart(newCart);
              if (data.code == 0) {
                alert(data.message);
              }
            } else {
              //未登录--将购物车中的数据保持localStorage中
              localStorage.setItem("cart", JSON.stringify(newCart));
            }
    
            //购物项项选中个数,购物车个数相同,全选需要选中
            let cartSize = this.cart.length;
            let checkedSize = this.cart.filter(item => item.checked).length;
            this.selectAllChecked = cartSize == checkedSize;
          },
          //true代表如果在 watch 里声明了之后,就会立即先去执行里面的handler方法
          immediate: false,
          //深度监听,常用于对象下面属性的改变
          deep: true
        }
      }
    
  • api.js

     //添加到购物车
        addToCart: (params) => {
            return axios.post("/gccartservice/carts", params)
        },
        getCart: () => {
            return axios.get('/gccartservice/carts')
        },
        updateCart: (params) => {
            return axios.put('/gccartservice/carts', params)
        },
    
  • flow1

    <template>
      <div>
        <!-- 顶部导航 start -->
        <TopNav></TopNav>
        <!-- 顶部导航 end -->
    
        <div style="clear:both;"></div>
    
        <!-- 页面头部 start -->
        <div class="header w990 bc mt15">
          <div class="logo w990">
            <h2 class="fl">
              <a href="index.html">
                <img src="images/logo.png" alt="畅购商城" />
              </a>
            </h2>
            <div class="flow fr">
              <ul>
                <li class="cur">1.我的购物车</li>
                <li>2.填写核对订单信息</li>
                <li>3.成功提交订单</li>
              </ul>
            </div>
          </div>
        </div>
        <!-- 页面头部 end -->
    
        <div style="clear:both;"></div>
    
        <!-- 主体部分 start -->
        <div class="mycart w990 mt10 bc">
          <h2>
            <span>我的购物车</span>
          </h2>
          <table>
            <thead>
              <tr>
                <th class="col0">
                  <input type="checkbox" :checked="selectAllChecked" @click="selectAll($event)" name id />
                </th>
                <th class="col1">商品名称</th>
                <th class="col2">商品信息</th>
                <th class="col3">单价</th>
                <th class="col4">数量</th>
                <th class="col5">小计</th>
                <th class="col6">操作</th>
              </tr>
            </thead>
            <tbody>
              <tr v-for="(goods,index) in cart" :key="index">
                <th class="col0">
                  <input type="checkbox" v-model="goods.checked" name id />
                </th>
                <td class="col1">
                  <a href>
                    <img :src="goods.midlogo" alt />
                  </a>
                  <strong>
                    <a href>{{goods.goods_name}}</a>
                  </strong>
                </td>
                <td class="col2">
                  <p
                    v-for="(value,key,index) in goods.spec_info_id_txt"
                    :key="index"
                  >{{key}} : {{value}}</p>
                </td>
                <td class="col3">
                  ¥
                  <span>{{goods.price.toFixed(2)}}</span>
                </td>
                <td class="col4">
                  <a href="javascript:;" @click.prevent="minus(goods)" class="reduce_num"></a>
                  <input
                    type="text"
                    name="amount"
                    value="1"
                    v-model="goods.count"
                    class="amount"
                    @keyup="updateCount(goods,$event)"
                  />
                  <a href="javascript:;" @click.prevent="plus(goods)" class="add_num"></a>
                </td>
                <td class="col5">
                  ¥
                  <span>{{(goods.price*goods.count).toFixed(2)}}</span>
                </td>
                <td class="col6">
                  <a href @click.prevent="del(index)">删除</a>
                </td>
              </tr>
            </tbody>
            <tfoot>
              <tr>
                <td colspan="7">
                  购物金额总计:
                  <strong>
                    ¥
                    <span id="total">{{totalPrice}}</span>
                  </strong>
                </td>
              </tr>
            </tfoot>
          </table>
          <div class="cart_btn w990 bc mt10">
            <a href class="continue">继续购物</a>
            <a class="checkout" @click.prevent="submit">结 算</a>
          </div>
        </div>
        <!-- 主体部分 end -->
    
        <div style="clear:both;"></div>
        <!-- 底部版权 start -->
        <Footer></Footer>
        <!-- 底部版权 end -->
      </div>
    </template>
    
    <script>
    import TopNav from "../components/TopNav";
    import Footer from "../components/Footer";
    export default {
      head: {
        title: "购物车页面",
        link: [{ rel: "stylesheet", href: "/style/cart.css" }],
        script: [{ type: "text/javascript", src: "/js/cart1.js" }]
      },
      components: {
        TopNav,
        Footer
      },
      data() {
        return {
          cart: [],
          selectAllChecked: false
        };
      },
      async mounted() {
        //获取token
        let token = sessionStorage.getItem("token");
        if (token) {
          //登录:服务器获得数据
          let { data } = await this.$request.getCart();
          this.cart = data.data;
          console.info(this.cart);
        } else {
          let cartStr = localStorage.getItem("cart");
    
          this.cart = JSON.parse(cartStr);
        }
      },
      methods: {
        minus: function(goods) {
          if (goods.count > 1) {
            goods.count--;
          }
        },
        plus: function(goods) {
          //可以考虑库存
          goods.count++;
        },
        updateCount: function(goods, e) {
          console.info(e.target.value);
          if (/^\d+$/.test(e.target.value)) {
            goods.count = e.target.value;
          } else {
            goods.count = 1;
          }
        },
        del(index) {
          if (confirm("您确定要删除么?")) {
            this.cart.splice(index, 1);
            console.info(this.cart);
          }
        },
        selectAll(e) {
          this.cart.forEach(item => {
            item.checked = e.target.checked;
          });
        },
        submit() {
          let token = sessionStorage.getItem("token");
          if (token) {
            this.$router.push("flow2");
          } else {
            //确定登录成功后调整的页面
            localStorage.setItem("returnURL", "flow2");
            //没有登录
            this.$router.push("login");
          }
        }
      },
      computed: {
        totalPrice() {
          let sum = 0;
          this.cart.forEach(g => {
            sum += g.price * g.count;
          });
          return sum;
        }
      },
      watch: {
        //深度监听
        cart: {
          async handler(newCart, oldCart) {
            //根据登录情况,更新购物车
            let token = sessionStorage.getItem("token");
            if (token) {
              //登录
              let { data } = await this.$request.updateCart(newCart);
              if (data.code == 0) {
                alert(data.message);
              }
            } else {
              //未登录--将购物车中的数据保持localStorage中
              localStorage.setItem("cart", JSON.stringify(newCart));
            }
    
            //购物项项选中个数,购物车个数相同,全选需要选中
            let cartSize = this.cart.length;
            let checkedSize = this.cart.filter(item => item.checked).length;
            this.selectAllChecked = cartSize == checkedSize;
          },
          //true代表如果在 watch 里声明了之后,就会立即先去执行里面的handler方法
          immediate: false,
          //深度监听,常用于对象下面属性的改变
          deep: true
        }
      }
    };
    </script>
    
    <style>
    </style>
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值