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>