商品详情页
功能介绍

数据


在商品详情页中除了以上数据,还包含SKU所属的SPU商品海报集合
简单总结下,商品详情页应该包含如下数据:
- SKU商品基本信息,包括名称,重量,默认图片
- SKU商品所属的三级类目信息
- SKU商品价格
- SKU所属SPU商品的销售属性以及销售属性值的组合
- SKU商品的图片集合
- SKU商品的平台属性,以及属性取值集合(规格参数)
- SKU所属SPU的商品海报
请求链路

实现
在商品服务中,我们需要接收前端发送的获取商品详情的请求,我们还需要重点实现以下接口,在业务层面处理商品详情页的服务调用请求
@GetMapping("goods/{skuId}") public Result<ProductDetailDTO> getItem(@PathVariable Long skuId){ ProductDetailDTO itemBySkuId = productDetailService.getItemBySkuId(skuId); return Result.ok(itemBySkuId); }public interface ProductDetailService {
/** * 获取sku详情信息 * @param skuId * @return */ ProductDetailDTO getItemBySkuId(Long skuId);}/* 该类封装了商品详情页所需的所有数据 */@Datapublic class ProductDetailDTO { // 商品sku 信息 + sku图片集合 SkuInfoDTO skuInfo;
// 获取指定的sku完整的销售属性值 List<SpuSaleAttributeInfoDTO> spuSaleAttrList;
// 获取spu中包含的所有的不同销售属性取值的组合 String valuesSkuJson;
// 获取sku商品的价格 BigDecimal price;
// 获取完整的三级类目信息 CategoryHierarchyDTO categoryHierarchy; // 获取sku所属的SPU商品的海报列表 List<SpuPosterDTO> spuPosterList; // 获取sku商品对应的平台属性集合(规格参数) List<SkuSpecification> skuAttrList;}获取SKU及其图片列表
在SkuService中,我们实现如下方法。
SkuInfoDTO getSkuInfo(Long skuId);这里,我们既需要查询,sku_info表中sku的基本信息,还需要查询出sku商品的图片列表,这里需要注意:
- 而在sku_image表中并没有存储sku商品图片对应的url,图片的url是存储在spu_image表中
- 所以要获取到sku商品的url,需要做联表查询,连接spu_image和sku_image两张表
- 通过联表查询,查询出sku商品图片的名称和url,封装在SkuImage对象中,得到List
<resultMap id="skuImage" type="com.cskaoyan.mall.product.model.SkuImage" autoMapping="true">
</resultMap>
<!-- 根据spuId 获取到销售属性值Id 与skuId 组成的数据集 --> <select id="getSkuImages" resultMap="skuImage"> select ski.id, ski.sku_id, ski.spu_img_id, ski.is_default, spi.img_name, spi.img_url from sku_image ski inner join spu_image spi on ski.spu_img_id=spi.id where ski.sku_id=#{skuId} </select>获取SKU所属的SPU销售属性集合

在SkuService中我们实现如下方法
List<SpuSaleAttributeInfoDTO> getSpuSaleAttrListCheckBySku(Long skuId, Long spuId);当点击某一个SKU商品,就会进入到商品详情页,在商品详情页中,可以显示该SKU商品对应的SPU中,所有销售属性值。这里,关于销售属性值,我们需要注意的是
- 我们需要获取当前SKU所属的SPU中,包含的所有销售属性和销售属性值
- 我们还要知道,当前浏览的这个SKU商品对应的销售属性值,并将其默认选中
如何实现呢?其实我们在查询销售属性值的时候,利用左外连接,就可以很快找到当前sku商品对应的所有销售属性值,并对其加以标记
- 首先,将spu_sale_attr_info与spu_sale_attr_value两张表做内连接, 其结果就包含指定了SPU所有销售属性及销售属性对应的销售属性值
select ssai.id, ssai.spu_id, ssai.sale_attr_id, ssai.sale_attr_name, spsav.id sale_attr_value_id, spsav.spu_sale_attr_id, spsav.spu_sale_attr_value_name from spu_sale_attr_info ssai inner join spu_sale_attr_value spsav on ssai.id = spsav.spu_sale_attr_id where ssai.spu_id=#{spuId}
- 紧接着,再用上一步的结果,与sku_sale_attr_value 做内连接,在这次内连接之后,我们就能SPU中包含的每个SKU商品的每个销售属性,以及销售属性值了
select ssai.id, ssai.spu_id, ssai.sale_attr_id, ssai.sale_attr_name, spsav.id sale_attr_value_id, spsav.spu_sale_attr_id, spsav.spu_sale_attr_value_name, sksav.sku_id from spu_sale_attr_info ssai inner join spu_sale_attr_value spsav on ssai.id = spsav.spu_sale_attr_id inner join sku_sale_attr_value sksav on sksav.spu_sale_attr_value_id = spsav.id where ssai.spu_id=24
但是这样还不够:
- 因为我们不仅仅要查询SPU中的每一个SKU商品的属性值
- 我们还想知道哪些销售属性值,是当前SKU的,因为前端需要默认选中它们,所以我们修改以上连接的SQL,修改连接方式为左外连接,并且增加连接条件
select ssai.id, ssai.spu_id, ssai.sale_attr_id, ssai.sale_attr_name, spsav.id sale_attr_value_id, spsav.spu_sale_attr_id, spsav.spu_sale_attr_value_name, sksav.sku_id from spu_sale_attr_info ssai inner join spu_sale_attr_value spsav on ssai.id = spsav.spu_sale_attr_id left join sku_sale_attr_value sksav on sksav.spu_sale_attr_value_id = spsav.id and sksav.sku_id=#{skuId}where ssai.spu_id=#{spuId}因为左外连接,有以下特征:
- 在SPU中包含的SKU属性值即使不满足连接条件,也会出现在连接结果中
- 不过不满足连接条件的那些SKU属性值的行,其sku_sale_attr_value表中字段值为null

所以利用这个特征,再结合第二个连接条件,我们就可以快速知道,哪些SKU属性值属于指定的SKU,因为只有指定的SKU销售属性值才会满足第二个连接条件,即sku_id这个字段值不会为null。完整的SQL如下:
<resultMap id="spuSaleAttrMap" type="com.cskaoyan.mall.product.model.SpuSaleAttributeInfo" autoMapping="true">
<id column="id" property="id"></id> <collection property="spuSaleAttrValueList" ofType="com.cskaoyan.mall.product.model.SpuSaleAttributeValue" autoMapping="true"> <id column="sale_attr_value_id" property="id"></id> </collection> </resultMap>
<select id="selectSpuSaleAttrListCheckedBySku" resultMap="spuSaleAttrMap">
select ssai.id, ssai.spu_id, ssai.sale_attr_id, ssai.sale_attr_name, spsav.id sale_attr_value_id, spsav.spu_sale_attr_id, spsav.spu_sale_attr_value_name, if(sksav.sku_id is null,0,1) is_checked
from spu_sale_attr_info ssai inner join spu_sale_attr_value spsav on ssai.spu_id = spsav.spu_id and ssai.id = spsav.spu_sale_attr_id left join sku_sale_attr_value sksav on sksav.spu_sale_attr_value_id = spsav.id and sksav.sku_id =#{skuId} where ssai.spu_id=#{spuId} order by ssai.id ,spsav.id </select>if(condition,true value,false value)是mysql中的一个函数,接收三个参数,第一个参数表示判断条件,在我们的SQL语句中,对应sksav.sku_id is null,第二个参数是判断条件为true时的结果,第三个参数是判断条件为false时的结果。
所以最终,我们的SQL语句执行结果中,只有指定SKU商品销售属性行的is_checked值为1,其他均为0
不同销售属性值组合

我们需要实现定义在SpuService接口中的方法
Map<String, Long> getSkuValueIdsMap(Long spuId);当点击某一个SKU商品,就会进入到商品详情页,在商品详情页中,可以显示该SKU商品对应的SPU中,所有销售属性值。这里,关于销售属性值,我们还需要注意,我们也可以在页面中,修改选中的销售属性值组合,从而跳转到不同的SKU商品详情页
所以,随之而来的问题就是,SKU商品所属的SPU销售属性值有多种不同的组合,而不同的SPU销售属性组合对应不同的SKU商品,我们如何让前端快速确定,什么销售属性组合对应那个SKU商品从而实现商品详情页的跳转呢?
- 首先,我们可以先获取,SPU中每一个SKU商品对应的所有销售属性值
select spsav.spu_sale_attr_id, sksav.spu_sale_attr_value_id, sksav.sku_id from sku_sale_attr_value sksav inner join spu_sale_attr_value spsav on spsav.id=sksav.spu_sale_attr_value_id where sksav.spu_id=#{spuId}
- 很显然,我们查询出的SKU商品属性值,应该不包含下架商品,所以还需要和sku_info表做连接查询,过滤下架商品
select spsav.spu_sale_attr_id, sksav.spu_sale_attr_value_id, sksav.sku_id from sku_sale_attr_value sksav inner join spu_sale_attr_value spsav on spsav.id=sksav.spu_sale_attr_value_id inner join sku_info on sksav.sku_id=sku_info.id and sku_info.is_sale=1 where sksav.spu_id=#{spuId}- 但是还不够,我们希望得到的是,SPU中销售属性组合 与 SKU的对应关系,所以继续修改SQL如下:
select group_concat(sksav.spu_sale_attr_value_id order by spsav.spu_sale_attr_id asc separator '|' ) sku_sale_attr_value_permutation, sku_id from sku_sale_attr_value sksav inner join spu_sale_attr_value spsav on spsav.id=sksav.spu_sale_attr_value_id inner join sku_info on sksav.sku_id=sku_info.id and sku_info.is_sale=1 where sksav.spu_id=#{spuId} group by sksav.sku_id
在以上SQL中,我们又给大家引入了mysql中的一个函数group_concat([DISTINCT] 要连接的字段 [Order BY ASC/DESC 排序字段] [Separator 分隔符])。
这个函数到底做什么呢,就是对指定字段的分组内,将字段值按照指定的顺序,用指定的分隔符拼接成一个字符串即可。假如有学生表student如下所示:
| id | name | class |
|---|---|---|
| 1 | 长风 | 1 |
| 2 | 景天 | 1 |
| 3 | 天明 | 2 |
| 4 | 空灵 | 2 |
| 5 | 远志 | 3 |
| 6 | 石头 | 3 |
如果我想查询出每个班级(class)对应的所有学生,可以定义如下SQL:
create table student ( id bigint primary key, name varchar(20), class int);
insert into student values(1, '长风', 1),(2, '景天', 1),(3, '天明', 2),(4, '空灵', 2),(5, '远志', 3),(6, '石头', 3);select group_concat(student.name order by id asc separator '|') names,class from student group by class;
相似的道理,如果我们能查询出一张类似的表,就可以清楚的知道销售属性值组合与sku的对应关系了
| 销售属性值组合 | sku_id |
|---|---|
| 销售属性值1|销售属性值2|… | 1 |
| 销售属性值1|销售属性值2|… | 2 |
| … | … |
所以,我们查询出的结果如下:

完整的sql定义以及结果映射如下:
<resultMap id="skuSaleAttrValuePermutation" type="com.cskaoyan.mall.product.model.SkuSaleAttributeValuePermutation" autoMapping="true">
</resultMap>
<!-- 根据spuId 获取到销售属性值Id 与skuId 组成的数据集 --> <select id="selectSaleAttrValuesBySpu" resultMap="skuSaleAttrValuePermutation"> select group_concat(sksav.spu_sale_attr_value_id order by spsav.spu_sale_attr_id asc separator '|' ) sku_sale_attr_value_permutation, sku_id from sku_sale_attr_value sksav inner join spu_sale_attr_value spsav on spsav.id=sksav.spu_sale_attr_value_id inner join sku_info on sksav.sku_id=sku_info.id and sku_info.is_sale=1 where sksav.spu_id=#{spuId} group by sksav.sku_id </select>获取SKU平台属性集合
在SkuService中实现如下方法
List<PlatformAttributeInfoDTO> getPlatformAttrInfoBySku(Long skuId); <resultMap id="platformAttrInfoMap" type="com.cskaoyan.mall.product.model.PlatformAttributeInfo" autoMapping="true"> <id column="id" property="id"></id> <!-- property: 实体类属性名 ofType: 实体类属性名对应的类型 --> <collection property="attrValueList" ofType="com.cskaoyan.mall.product.model.PlatformAttributeValue" autoMapping="true"> <id column="attr_value_id" property="id"></id> </collection>
</resultMap>
<select id="selectPlatformAttrInfoListBySkuId" resultMap="platformAttrInfoMap">
select pai.id, pai.attr_name, pai.category_id, pai.category_level, pav.id attr_value_id, pav.value_name, pav.attr_id
from platform_attr_info pai inner join platform_attr_value pav on pai.id=pav.attr_id inner join sku_platform_attr_value spav on spav.value_id=pav.id where spav.sku_id=#{skuId}
</select>三级类目
在skuinfo表中,针对每一个SKU商品,我们都记录了其对应的第三级类目的id,所以我们就可以顺藤摸瓜,查询出该SKU商品所属的第一,二级目录,这样我们就可以查询出,包含该SKU商品的三级目录信息了。为此,我们需要实现CategoryService中以下接口方法
/* 获取和该三级类目相关完整信息(包括它所属的一级类目和二级类目) */ CategoryHierarchyDTO getCategoryViewByCategoryId(Long thirdLevelCategoryId);@Datapublic class CategoryHierarchyDTO { // 一级类目id private Long firstLevelCategoryId; // 一级类目名称 private String firstLevelCategoryName; // 二级类目id private Long secondLevelCategoryId; // 二级类目名称 private String secondLevelCategoryName; // 三级类目id private Long thirdLevelCategoryId; // 三级类目名称 private String thirdLevelCategoryName;}这里我们可以定义定义多表查询
<resultMap id="skuCategoryHierarchyInfo" type="com.cskaoyan.mall.product.model.CategoryHierarchy" autoMapping="true">
</resultMap>
<!-- 1. 若thirdLevelCategoryId为null,则查询所有三级类目对应的一二级目录 2. 若thirdLevelCategoryId不为null,则查询指定三级类目对应的一二级类目 --> <select id="selectCategoryHierarchy" resultMap="skuCategoryHierarchyInfo"> select c1.id as first_level_category_id, c1.name as first_level_category_name, c2.id as second_level_category_id, c2.name as second_level_category_name, c3.id as third_level_category_id, c3.name as third_level_category_name from first_level_category c1 inner join second_level_category c2 on c2.first_level_category_id = c1.id inner join third_level_category c3 on c3.second_level_category_id = c2.id <where> <if test="thirdLevelCategoryId != null"> c3.id=#{thirdLevelCategoryId} </if> </where> </select>总代码流程梳理
@Autowired SpuService spuService;
@Autowired SkuService skuService;
@Autowired CategoryService categoryService;
@Override public ProductDetailDTO getItemBySkuId(Long skuId) { // 封装商品详情页中所有的数据 ProductDetailDTO productDetailDTO = new ProductDetailDTO();
// 通过skuId 查询skuInfo(还需要包含sku商品的图片列表) SkuInfoDTO skuInfo = ...; productDetailDTO.setSkuInfo(skuInfo);
// 根据spuI的查询SPU所有销售属性及销售属性值,根据skuId默认选中指定的SKU商品属性值 List<SpuSaleAttributeInfoDTO> spuSaleAttrListCheckBySku = ...; productDetailDTO.setSpuSaleAttrList(spuSaleAttrListCheckBySku);
// 根据spuId 查询销售属性值组合与sku_id对应关系 Map<String, Long> skuValueIdsMap = ...; // 将map转化为json字符串 String valuesSkuJson = JSON.toJSONString(skuValueIdsMap); // 保存valuesSkuJson productDetailDTO.setValuesSkuJson(valuesSkuJson);
//获取商品最新价格 BigDecimal skuPrice = ...; productDetailDTO.setPrice(skuPrice);
//获取sku商品的三级类目信息 CategoryHierarchyDTO categoryViewByCategory = ...; //分类信息 productDetailDTO.setCategoryHierarchy(categoryViewByCategory);
// 获取spu海报数据 List<SpuPosterDTO> spuPosterBySpuId = ...; productDetailDTO.setSpuPosterList(spuPosterBySpuId);
// 获取sku平台属性 List<PlatformAttributeInfoDTO> platformAttrInfoBySku = ...; // 将平台属性转化为规格数据 List<SkuSpecification> skuAttrList = platformAttrInfoBySku.stream().map((baseAttrInfo) -> { // 创建封装规格数据的对象 SkuSpecification skuSpecification = new SkuSpecification(); // 设置属性名 skuSpecification.setAttrName(baseAttrInfo.getAttrName()); // 设置属性值 skuSpecification.setAttrValue(baseAttrInfo.getAttrValueList().get(0).getValueName());
return skuSpecification; }).collect(Collectors.toList()); productDetailDTO.setSkuAttrList(skuAttrList);
// 商品被浏览了一次,可以增加它的热度积分(后面实现)
return productDetailDTO; }文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!