项目
博客
文档
归档
资源链接
关于我
项目
博客
文档
归档
资源链接
关于我
24| 商品(购物车)微服务 - 电商知识&购物车及商品接口开发
2024-08-09
·
·
原创
·
·
本文共 1,531个字,预计阅读需要 6分钟。
### 电商知识 - `类目`:一个`树状结构的系统`,根据业务可以分成4-5级。如手机->智能手机->国产手机 类目,在这里面,手机是一级类目,国产手机是三级类目,也是叶子类目 - SPU - `Standard Product Unit`:`标准化产品单元`。是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。通俗点讲,`属性值、特性相同的商品就可以称为一个SPU`。比如 Iphone100 就是一个SPU - SKU:一般指`库存保有单位`。库存保有单位即`库存进出计量的单位`, 可以是以件、盒、托盘等为单位。*SKU*是物理上不可分割的最小存货单元,在服装、鞋类商品中使用最多最普遍,买家购买、商家进货、供应商备货、工厂生产都是依据SKU进行的。比如Iphone100 128g 土豪金就是一个SKU 商品: 首页商品分页展示,商品详情展示,库存控制。 购物车:加入购物车,清空购物车,修改购物车数量。 **购物车项目核心知识**: - 商品微服务库存管理 - 分布式事务 - 电商平台购物车的实现方案 - 通用购物车数据结构设计 - 购物车价格统计业务逻辑梳理 - 重点知识下沉到订单微服务 - 商品库存锁定和回收 - 优惠券使用锁定和回收 - 订单验价 - 多通道支付设计等 商品服务数据库p_nla_product ```sql CREATE TABLE `banner` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `img` varchar(524) DEFAULT NULL COMMENT '图片', `url` varchar(524) DEFAULT NULL COMMENT '跳转地址', `weight` int(11) DEFAULT NULL COMMENT '权重', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='banner表'; INSERT INTO `banner` (`id`, `img`, `url`, `weight`) VALUES (1, 'https://file.xdclass.net/video/2020/alibabacloud/zx-lbt.jpeg', 'https://m.xdclass.net/#/member', 1), (2, 'https://file.xdclass.net/video/%E5%AE%98%E7%BD%91%E8%BD%AE%E6%92%AD%E5%9B%BE/20%E5%B9%B4%E5%8F%8C11%E9%98%BF%E9%87%8C%E4%BA%91/fc-lbt.jpeg', 'https://www.aliyun.com/1111/pintuan-share?ptCode=MTcwMTY3MzEyMjc5MDU2MHx8MTE0fDE%3D&userCode=r5saexap', 3), (3, 'https://file.xdclass.net/video/%E5%AE%98%E7%BD%91%E8%BD%AE%E6%92%AD%E5%9B%BE/20%E5%B9%B4%E5%8F%8C11%E9%98%BF%E9%87%8C%E4%BA%91/FAN-lbu-vip.jpeg', 'https://file.xdclass.net/video/%E5%AE%98%E7%BD%91%E8%BD%AE%E6%92%AD%E5%9B%BE/Nginx.jpeg', 2); CREATE TABLE `product` ( `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT, `title` varchar(128) DEFAULT NULL COMMENT '标题', `cover_img` varchar(128) DEFAULT NULL COMMENT '封面图', `detail` varchar(256) DEFAULT '' COMMENT '详情', `old_price` decimal(16,2) DEFAULT NULL COMMENT '老价格', `price` decimal(16,2) DEFAULT NULL COMMENT '新价格', `stock` int(11) DEFAULT NULL COMMENT '库存', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `lock_stock` int(11) DEFAULT '0' COMMENT '锁定库存', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表'; INSERT INTO `product` (`id`, `title`, `cover_img`, `detail`, `old_price`, `price`, `stock`, `create_time`, `lock_stock`) VALUES (1, '小滴课堂抱枕', 'https://file.xdclass.net/video/2020/alibabacloud/zt-alibabacloud.png', 'https://file.xdclass.net/video/2021/60-MLS/summary.jpeg', 32.00, 213.00, 100, '2021-09-12 00:00:00', 31), (2, '技术人的杯子Linux', 'https://file.xdclass.net/video/2020/alibabacloud/zt-alibabacloud.png', 'https://file.xdclass.net/video/2021/59-Postman/summary.jpeg', 432.00, 42.00, 20, '2021-03-12 00:00:00', 2), (3, '技术人的杯子docker', 'https://file.xdclass.net/video/2020/alibabacloud/zt-alibabacloud.png', 'https://file.xdclass.net/video/2021/60-MLS/summary.jpeg', 35.00, 12.00, 20, '2022-09-22 00:00:00', 13), (4, '技术人的杯子git', 'https://file.xdclass.net/video/2020/alibabacloud/zt-alibabacloud.png', 'https://file.xdclass.net/video/2021/60-MLS/summary.jpeg', 12.00, 14.00, 20, '2022-11-12 00:00:00', 2); ``` **商品首页分页**,**商品首页分页列表**开发。 ### 常见购物车功能 购物车里面的购物项常见字段 - user_id、product_id、buy_num 购物车常见实现方式 - 实现方式一: 存储到数据库:性能存在瓶颈 - 实现方式二: 前端本地存储-`localstorage-sessionstorage` - localstorage在浏览器中存储 key/value 对,没有过期时间。 - sessionstorage在浏览器中存储 key/value 对,在关闭会话窗口后将会删除这些数据。 - 实现方式三: 后端存储到缓存如redis:可以开启AOF持久化防止重启丢失(推荐) - 实现方式四: 后端存储到缓存如redis-并同步更新到数据库 - 大家可能会想到缓存和数据库的一致性,加了用户唯一标识后,没高并发操作同一数据的场景 **购物车和购物项VO类**: - 一个购物车里面,存在多个购物项 - CartVO 购物车 - 商品总件数 - 整个购物车总价 - 实际支付总价 ```java @Data public class CartVO { /** * 购物项 */ @JsonProperty("cart_items") private List
cartItems; /** * 购买总件数 */ @JsonProperty("total_num") private Integer totalNum; /** * 购物车总价格 */ @JsonProperty("total_price") private BigDecimal totalPrice; /** * 购物车实际支付价格 */ @JsonProperty("real_pay_price") private BigDecimal realPayPrice; /** * 总件数 */ public Integer getTotalNum() { if (this.cartItems != null) { return cartItems.stream().mapToInt(CartItemVO::getBuyNum).sum(); } return 0; } /** * 总价格 */ public BigDecimal getTotalPrice() { BigDecimal amount = new BigDecimal("0"); if (this.cartItems != null) { for (CartItemVO cartItemVO : cartItems) { BigDecimal itemTotalAmount = cartItemVO.getTotalAmount(); amount = amount.add(itemTotalAmount); } } return amount; } /** * 购物车里面实际支付的价格 */ public BigDecimal getRealPayPrice() { BigDecimal amount = new BigDecimal("0"); if (this.cartItems != null) { for (CartItemVO cartItemVO : cartItems) { BigDecimal itemTotalAmount = cartItemVO.getTotalAmount(); amount = amount.add(itemTotalAmount); } } return amount; } } ``` - CartItemVO 购物项 - 商品id - 购买数量 - 商品标题(冗余) - 商品图片(冗余) - 商品单价 - 总价格 ( 单价*数量 ) ```java public class CartItemVO { /** * 商品id */ @JsonProperty("product_id") private Long productId; /** * 购买数量 */ @JsonProperty("buy_num") private Integer buyNum; /** * 商品标题 */ @JsonProperty("product_title") private String productTitle; /** * 图片 */ @JsonProperty("product_img") private String productImg; /** * 商品单价 */ private BigDecimal amount; /** * 总价格,单价+数量 */ @JsonProperty("total_amount") private BigDecimal totalAmount; } ``` ### 购物车redis数据结构 - 一个购物车里面,存在多个购物项 - 所以购物车结构是一个双层Map: - Map
> - 第一层Map,Key是用户id - 第二层Map,Key是购物车中商品id,值是购物车数据 ### 添加/清空购物车接口 请求对象 ```java @ApiModel(value = "添加购物车对象", description = "添加购物车请求对象") @Data public class CartItemRequest { @ApiModelProperty(value = "商品id", example = "11") @JsonProperty("product_id") private long productId; @ApiModelProperty(value = "购买数量", example = "1") @JsonProperty("buy_num") private int buyNum; } ``` controller层 ```java @Api(tags = "购物车控制器") @RestController @RequestMapping("/pdt/cart/v1") public class CartController { @Resource private CartService cartService; @ApiOperation("添加到购物车") @PostMapping("add") public JsonData addToCart(@ApiParam("购物项") @RequestBody CartItemRequest cartItemRequest) { cartService.addToCart(cartItemRequest); return JsonData.buildSuccess(); } } ``` service层:抽取我的购物车,通用方法缓存获取 在配置RedisConfig之后,**BoundHashOperations
**中get获取数据时,对应的key要转换**为字符串**,否则报错**myCart.get(String.valueOf(productId))** ```java @Service public class CartServiceImpl implements CartService { @Resource private ProductService productService; @Resource private RedisTemplate redisTemplate; @Override public void addToCart(CartItemRequest cartItemRequest) { long productId = cartItemRequest.getProductId(); int buyNum = cartItemRequest.getBuyNum(); //获取购物车 BoundHashOperations
myCart = getMyCartOps(); Object cacheObj = myCart.get(String.valueOf(productId)); String result = ""; if (cacheObj != null) { result = (String) cacheObj; } if (StringUtils.isBlank(result)) { //不存在则新建一个商品 CartItemVO cartItemVO = new CartItemVO(); ProductVO product = productService.findDetailById(productId); if (product == null) { throw new BizException(BizCodeEnum.CART_FAIL); } cartItemVO.setAmount(product.getAmount()); cartItemVO.setBuyNum(buyNum); cartItemVO.setProductId(productId); cartItemVO.setProductImg(product.getCoverImg()); cartItemVO.setProductTitle(product.getTitle()); cartItemVO.setTotalAmount(product.getAmount().multiply(new BigDecimal(buyNum))); myCart.put(String.valueOf(productId), JSON.toJSONString(cartItemVO)); } else { //存在商品,修改数量 CartItemVO cartItem = JSON.parseObject(result, CartItemVO.class); cartItem.setBuyNum(cartItem.getBuyNum() + buyNum); myCart.put(String.valueOf(productId), JSON.toJSONString(cartItem)); } } /** * 抽取我的购物车,通用方法 */ private BoundHashOperations
getMyCartOps() { return redisTemplate.boundHashOps(getCartKey()); } /** * 购物车 key */ private String getCartKey() { LoginUser loginUser = LoginInterceptor.threadLocal.get(); return String.format(CacheKey.CART_KEY, loginUser.getId()); } } ``` 依赖的商品服务 ```java @Service public class ProductServiceImpl extends ServiceImpl
implements ProductService { @Override public ProductVO findDetailById(Long productId) { return beanProcess(baseMapper.selectById(productId)); } private ProductVO beanProcess(ProductEntity product) { ProductVO result = new ProductVO(); BeanUtils.copyProperties(product, result); result.setAmount(product.getPrice()); result.setStock(product.getStock() - product.getLockStock()); return result; } } ``` ### redis乱码问题和清空购物车接口开发 redis存储乱码问题 - 默认使用JdkSerializationRedisSerializer进行序列化 ```java /** * Redis配置:解决乱码问题 */ @Configuration public class RedisConfig { @Bean public RedisTemplate
redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate
template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); // 使用StringRedisSerializer序列化键 template.setKeySerializer(new StringRedisSerializer()); // 使用GenericJackson2JsonRedisSerializer序列化值 ObjectMapper om = new ObjectMapper(); // 可以自定义ObjectMapper配置,确保满足你项目的需求 template.setValueSerializer(new GenericJackson2JsonRedisSerializer(om)); // 设置hash的键和值的序列化器 template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer(om)); // 使序列化器生效 template.afterPropertiesSet(); return template; } } ``` - 修改key-value序列化方式,hash结构不修改 ```java /** * 清空购物车 */ @Override public void clear() { String cartKey = getCartKey(); redisTemplate.delete(cartKey); } ``` **查看我的购物车**/**删除购物项和修改购物车数量接口开发** ```java @ApiOperation("清空购物车") @DeleteMapping("/clear") public JsonData cleanMyCart() { cartService.clear(); return JsonData.buildSuccess(); } @ApiOperation("查看我的购物车") @GetMapping("/my-cart") public JsonData findMyCart() { return JsonData.buildSuccess(cartService.getMyCart()); } @ApiOperation("修改购物车数量") @PostMapping("change") public JsonData changeItemNum(@ApiParam("购物项") @RequestBody CartItemRequest cartItemRequest) { cartService.changeItemNum(cartItemRequest); return JsonData.buildSuccess(); } @ApiOperation("删除购物项") @DeleteMapping("/delete/{product_id}") public JsonData deleteItem(@ApiParam(value = "商品id", required = true) @PathVariable("product_id") long productId) { cartService.deleteItem(productId); return JsonData.buildSuccess(); } ``` ```java /** * 删除购物项 */ @Override public void deleteItem(long productId) { getMyCartOps().delete(String.valueOf(productId)); } @Override public void changeItemNum(CartItemRequest cartItemRequest) { BoundHashOperations
mycart = getMyCartOps(); Object cacheObj = mycart.get(String.valueOf(cartItemRequest.getProductId())); if (cacheObj == null) { throw new BizException(BizCodeEnum.CART_FAIL); } String obj = (String) cacheObj; CartItemVO cartItemVO = JSON.parseObject(obj, CartItemVO.class); cartItemVO.setBuyNum(cartItemRequest.getBuyNum()); cartItemVO.setTotalAmount(cartItemVO.getAmount().multiply(new BigDecimal(cartItemVO.getBuyNum()))); mycart.put(String.valueOf(cartItemRequest.getProductId()), JSON.toJSONString(cartItemVO)); } @Override public CartVO getMyCart() { //获取全部购物项 List
cartItemVOList = buildCartItem(false); CartVO cartVO = new CartVO(); cartVO.setCartItems(cartItemVOList); return cartVO; } /** * 获取最新的购物项, * * @param latestPrice 是否获取最新价格 */ private List
buildCartItem(boolean latestPrice) { BoundHashOperations
myCart = getMyCartOps(); List
itemList = myCart.values(); List
cartItemVOList = new ArrayList<>(); //拼接id列表查询最新价格 List
productIdList = new ArrayList<>(); assert itemList != null; for (Object item : itemList) { CartItemVO cartItemVO = JSON.parseObject((String) item, CartItemVO.class); cartItemVOList.add(cartItemVO); productIdList.add(cartItemVO.getProductId()); } //查询最新的商品价格 if (latestPrice) { setProductLatestPrice(cartItemVOList, productIdList); } return cartItemVOList; } /** * 设置商品最新价格 */ private void setProductLatestPrice(List
cartItemVOList, List
productIdList) { //批量查询 List
productVOList = productService.findProductsByIdBatch(productIdList); //分组 Map
maps = productVOList.stream().collect(Collectors.toMap(ProductVO::getId, Function.identity())); cartItemVOList.forEach(item -> { ProductVO productVO = maps.get(item.getProductId()); item.setProductTitle(productVO.getTitle()); item.setProductImg(productVO.getCoverImg()); item.setAmount(productVO.getAmount()); }); } ``` ```java @Override public List
findProductsByIdBatch(List
productIdList) { return baseMapper.selectList(new QueryWrapper
().in("id", productIdList)) .stream().map(item -> { ProductVO result = new ProductVO(); BeanUtils.copyProperties(item, result); result.setStock(item.getStock() - item.getLockStock()); return result; }).collect(Collectors.toList()); } ```