商品详情页

2821 字
14 分钟
商品详情页

功能介绍#

数据#

在商品详情页中除了以上数据,还包含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);
}
/*
该类封装了商品详情页所需的所有数据
*/
@Data
public 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如下所示:

idnameclass
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

所以,我们查询出的结果如下:

![](/assets/firefly-docs/microservice/microservice-13-product-detail/group_concat sku查询示例.png)

完整的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);
@Data
public 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;
}

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

商品详情页
https://firefly-mu-weld.vercel.app/posts/microservice-13-product-detail/
作者
Daisy
发布于
2026-06-14
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
Daisy
Hello, I'm Daisy.
公告
欢迎来到我的博客!这是一则示例公告。
分类
标签

文章目录