努力好了,时间会给你答案。--------magic_guo

购物车模块的设计方式有很多种:
1、用户登录或者未登录
① 登录将购物车放入redis,未登录将购物车放入Cookie中
② 登录将购物车放入mysql,未登录将购物车存入redis
2、强制用户登录
除了首页和商品详情页,其他的模块访问都会做限制,以达到用户登录的效果;
但无论如何,购物车信息只是一个暂存的信息,创建订单后,会将购物车的信息删除,而且购物车访问的频率会很大,对于性能也有一定的要求,这是购物车数据库的设计的两个重要因素;

一般来说,购物车模块需要存储的重要信息分为两个:用户和商品,也就是用户id和商品id。如果放在redis中,则使用hash结构来存储比较方便;如果放在Cookie中,因为cookie是使用键值对存储的,其结构也类似于map或者hash;如果mysql中,则牵涉到数据库表的设计;

本文所采用的是mysql+redis的方案,如下图:

在这里插入图片描述
如上图所表示,用户登录则将购物车放入mysql中,未登录则放入redis中,那问题来了,怎么区分用户是否登录?
上文在sso单点登录模块,我们在登录时,会返回给浏览器一个token,下次用户登录在请求头中携带一个字段为Authorization的token,在路由网关验证token,则可以访问其他的模块;
那么在购物车模块,我们以AOP面向切面编程思想,做了一个@LoginUser的装饰器,来验证用户是否登录,如果获取到token,则会注入user,没有携带token则user的注入为null;凡是方法上添加了@LoginUser注解,则在方法执行前都会做一个登录的验证,返回一个user;

@Documented // 生成API文档
@Target({ElementType.METHOD}) // 这个注解可以添加到那些元素上
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {
}

实现类:
在这里,我们使用环绕通知,来实现此功能,
@Around的作用
1、既可以在目标方法之前织入增强动作,也可以在执行目标方法之后织入增强动作;
2、可以决定目标方法在什么时候执行,如何执行,甚至可以完全阻止目标目标方法的执行;
3、可以改变执行目标方法的参数值,也可以改变执行目标方法之后的返回值; 当需要改变目标方法的返回值时,只能使用Around方法;

注意事项:虽然Around功能强大,但通常需要在线程安全的环境下使用。因此,如果使用普通的Before、AfterReturing增强方法就可以解决的事情,就没有必要使用Around增强处理了。

@Component
@Aspect  // 标识 这是一个切面
public class LoginUserImpl {

    @Autowired
    private HttpServletRequest request;

    @Around("@annotation(loginUser)")
    public Object loginUser(ProceedingJoinPoint point, LoginUser loginUser) throws Exception {

        // 封装的登录的对象
        User user = null;

        // 1.获取token
        String token = request.getHeader("Authorization");

        // 如果token在请求头中没有获取到,再看看地址栏中是否存在
        if (StringUtils.isEmpty(token)) {
            token = request.getParameter("token");
        }

        Object[] args = point.getArgs(); //因为方法有多个形参

        if (!StringUtils.isEmpty(token)) {
            // 解析token
            DecodedJWT verify = JWTUtils.verify(token);

            // 从token中获取uid
            String id = verify.getClaim("id").asString();

            // 把uid封装到User对象中
            user = new User();
            user.setId(Integer.parseInt(id));

            // 2.遍历数组,找到user的参数
            for (int i = 0; i < args.length; i++) {
                if (args[i] != null && args[i].getClass() == User.class) {
                    args[i] = user;
                    break; // 替换完成就马上返回
                }
            }
        }

        try {
            // 放行
            return point.proceed(args);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return user;
    }
}

maven依赖:

 <dependencies>
        <dependency>
            <groupId>com.guo</groupId>
            <artifactId>shop-common</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

配置:

spring:
  cloud:
    config:
      uri: http://localhost:9999
      profile: shop-car, eureka-client, log, datasource, redis
      name: application
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.guo

各个层级的代码:
Controller层:

@RequestMapping("/car")
@RestController
@Slf4j
public class CarController {

    @Autowired
    private ICarService carService;

    @Autowired
    private RedisTemplate redisTemplate;

    @PostConstruct
    public void init() {
        // 设置key的序列化方式为字符串
        redisTemplate.setKeySerializer(new StringRedisSerializer());
    }

    @RequestMapping("/addCarMysql")
    public ResultEntity addCarMysql(@RequestBody Car car) {
        return ResultEntity.success(carService.insert(car));
    }

    @LoginUser  // 代表这个接口上需要注入当前登录用户(如果登录就注入user, 没有登录就注入null)
    @RequestMapping("/addCar")
    @ResponseBody
    public ResultEntity addCar(@CookieValue(name = ShopConstants.ANON_ID, required = false) String anonId, Car car, User user, HttpServletResponse response) {
        // 判断用户是否登录
        log.info("user:{}", user);
        if (user.getId() != null) {
            // 插入数据库,并判断数据库中是否存在此数据,如果存在则更新数量,如果不存在直接加入
            car.setUid(user.getId());
            carService.addCarMysql(car);
        }  else {
            // 如果没有匿名用户的唯一id,生成一个匿名用户的唯一标识uuid
            if (StringUtils.isEmpty(anonId)) {
                anonId = UUID.randomUUID().toString();
                // 创建一个cookie
                Cookie cookie = new Cookie(ShopConstants.ANON_ID, anonId);
                // 解决cookie跨域问题
                cookie.setPath("/");
                // 设置cookie的有效时间为7天
                cookie.setMaxAge(60 * 60 * 24 * 7);
                // 将cookie响应给浏览器
                response.addCookie(cookie);
            }
            // 插入redis中,使用hash数据结构(相当于map)
            if (redisTemplate.opsForHash().hasKey(anonId, car.getGid())) {
                Object count = redisTemplate.opsForHash().get(anonId, car.getGid());
                assert count != null;
                car.setGcount(Integer.parseInt(count.toString()) + car.getGcount());
            }
            log.info("anonId:{}", anonId);
            redisTemplate.opsForHash().put(anonId, car.getGid(), car.getGcount());

        }
        return ResultEntity.success();
    }

    @LoginUser
    @RequestMapping("/getCarList")
    public ResultEntity getCarList(@CookieValue(name = ShopConstants.ANON_ID, required = false) String anonId, User user) {

        List<CarGoods> carArrayList = new ArrayList<>();

        // 判断用户是否登录
        if (user.getId() != null) {
            // 查询数据库
            carArrayList = carService.getCarList(user.getId());
        } else {
            // 查询redis
            if (!StringUtils.isEmpty(anonId)) {
                Set keys = redisTemplate.opsForHash().keys(anonId);
                if (!keys.isEmpty()) {
                    for (Object gid : keys) {
                        Object count = redisTemplate.opsForHash().get(anonId, gid);
                        CarGoods carGoods = carService.getCarGoodsByGid(gid);
                        assert count != null;
                        carGoods.setCount(Integer.parseInt(count.toString()));
                        carArrayList.add(carGoods);
                    }
                }
            }
        }
        return ResultEntity.success(carArrayList);
    }

    @LoginUser
    @RequestMapping("/updateCar")
    public ResultEntity updateCar(User user, @CookieValue(name = ShopConstants.ANON_ID, required = false) String anonId, Integer gid, Integer count) {

        if (user.getId() != null) {
            // 更新数据库
            Car car = new Car();
            car.setGcount(count);
            EntityWrapper<Car> carEntityWrapper = new EntityWrapper<>();
            carEntityWrapper.eq("uid", user.getId());
            carEntityWrapper.eq("gid", gid);

            // 根据用户id和商品id来修改购物车的中商品的数量
            carService.update(car, carEntityWrapper);
        } else {
            redisTemplate.opsForHash().put(anonId, gid, count);
        }

        return ResultEntity.success();
    }

    @LoginUser
    @RequestMapping("/showCarNum")
    public ResultEntity showCarNum(@CookieValue(name = ShopConstants.ANON_ID, required = false) String anonID, User user) {
        int count = 0;
        if (user.getId() != null) {
            EntityWrapper<Car> carEntityWrapper = new EntityWrapper<>();
            carEntityWrapper.eq("uid", user.getId());
            count = carService.selectCount(carEntityWrapper);
        }else {
            count = redisTemplate.opsForHash().keys(anonID).size();
        }

        return ResultEntity.success(count);
    }

    @RequestMapping("/deleteCarItems")
    @LoginUser
    public ResultEntity deleteCarItems(User user, @CookieValue(name = ShopConstants.ANON_ID, required = false) String anonId, Integer gid) {

        if (user.getId() != null) {
            EntityWrapper<Car> carEntityWrapper = new EntityWrapper<>();
            carEntityWrapper.eq("uid", user.getId());
            carEntityWrapper.eq("gid", gid);
            carService.delete(carEntityWrapper);
        }else {
            redisTemplate.opsForHash().delete(anonId, gid);
        }

        return ResultEntity.success();
    }

    @RequestMapping("/clearCar")
    @LoginUser
    public ResultEntity clearCar(User user, @CookieValue(name = ShopConstants.ANON_ID, required = false) String anonId) {
        if (user.getId() != null) {
            EntityWrapper<Car> carEntityWrapper = new EntityWrapper<>();
            carEntityWrapper.eq("uid", user.getId());
            // 删除此用户下的所有购物车商品
            carService.delete(carEntityWrapper);
        } else {
            // 将此hash结构删除掉
            Set keys = redisTemplate.opsForHash().keys(anonId);
            for (Object key : keys) {
                redisTemplate.opsForHash().delete(anonId, key);
            }
        }
        return ResultEntity.success();
    }
}

Service interface层:

public interface ICarService extends IService<Car> {
    Integer addCarMysql(Car car);

    ArrayList<CarGoods> getCarList(Integer id);

    CarGoods getCarGoodsByGid(Object gid);
}

ServiceImpl层:

@Service
@Slf4j
public class CarServiceImpl extends ServiceImpl<CarMapper, Car> implements ICarService {

    @Override
    public Integer addCarMysql(Car car) {

        Integer numberOfAffectedRows;

        // 查询数据库中此用户是否有此类商品(防止用户一直在点添加购物车,而不是直接点数量)
        EntityWrapper<Car> carEntityWrapper1 = new EntityWrapper<>();
        carEntityWrapper1.eq("uid", car.getUid());
        carEntityWrapper1.eq("gid", car.getGid());
        List<Car> cars = baseMapper.selectList(carEntityWrapper1);
        if (cars.size() > 0) {
            Car dbCar = cars.get(0);
            car.setGcount(car.getGcount() + dbCar.getGcount());
            EntityWrapper<Car> carEntityWrapper = new EntityWrapper<>();
            carEntityWrapper.eq("uid", car.getUid());
            carEntityWrapper.eq("gid", car.getGid());
            numberOfAffectedRows = baseMapper.update(car, carEntityWrapper);
        } else {
            numberOfAffectedRows = baseMapper.insert(car);
        }
        return numberOfAffectedRows;
    }

    @Override
    public ArrayList<CarGoods> getCarList(Integer uid) {
        return baseMapper.getCarList(uid);
    }

    @Override
    public CarGoods getCarGoodsByGid(Object gid) {
        return baseMapper.getCarGoodsByGid(gid);
    }
}

mapper层:

public interface CarMapper extends BaseMapper<Car> {
    ArrayList<CarGoods> getCarList(Integer uid);

    CarGoods getCarGoodsByGid(Object gid);
}

mapper.xml层:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.guo.mapper.CarMapper">

    <resultMap id="baseResultMap" type="com.guo.domain.CarGoods">
        <result column="gcount" property="count"/>
        <result column="gname" property="gname"/>
        <result column="gprice" property="gprice"/>
        <result column="gid" property="gid"/>
        <result column="gdesc" property="gdesc"/>

        <!-- 对多-->
        <collection property="goodsPicList" ofType="com.guo.entity.GoodsPng">
            <id column="gpid" property="id"/>
            <result column="gid" property="gid"/>
            <result property="png" column="png"/>
        </collection>
    </resultMap>

    <select id="getCarList" resultMap="baseResultMap">
        select
            c.*, g.*, gp.id as gpid
        from
            t_car as c
        left join t_goods as g on (c.gid=g.id)
        left join t_goods_pic as gp on (g.id=gp.gid)
        where c.uid = #{uid}
    </select>

    <select id="getCarGoodsByGid" resultMap="baseResultMap">
        select
            g.*, gp.id as gpid, gp.gid, gp.png
        from t_goods as g
        left join t_goods_pic as gp on (g.id=gp.gid)
        where g.id = #{gid}
    </select>
</mapper>

启动类:

@SpringBootApplication(scanBasePackages = "com.guo")
@EnableEurekaClient
@MapperScan("com.guo.mapper")
public class ShopCarApplication {

    public static void main(String[] args) {
        SpringApplication.run(ShopCarApplication.class, args);
    }

}

对于模块的设计,一定要理清思路和流程,这样的话,在编码的时候,就不会有疑问,至少大致流程是没问题的;以上接口都经过测试无误,有什么问题,请批评指正!


本文章教学视频来自:https://www.bilibili.com/video/BV1tb4y1Q74E?p=3&t=125


静下心,慢慢来,会很快!

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐