Skip to content

商品详情 ​

1 数据 ​

在商品详情页中除了以上数据,还包含SKU所属的SPU商品海报集合

简单总结下,商品详情页应该包含如下数据:

  • SKU商品基本信息,包括名称,重量,默认图片
  • SKU商品的图片集合
  • SKU商品所属的三级类目信息
  • SKU商品价格
  • SKU所属SPU商品的销售属性以及销售属性值的组合
  • SKU商品的平台属性,以及属性取值集合(规格参数)
  • SKU所属SPU的商品海报

2 请求链路 ​

3 实现 ​

在商品服务中,我们需要接收前端发送的获取商品详情的请求,我们还需要重点实现以下接口,在业务层面处理商品详情页的服务调用请求

java
    @GetMapping("goods/{skuId}")
    public Result<ProductDetailDTO> getItem(@PathVariable Long skuId){
        ProductDetailDTO itemBySkuId = productDetailService.getItemBySkuId(skuId);
        return Result.ok(itemBySkuId);
    }
java
public interface ProductDetailService {

    /**
     * 获取sku详情信息
     * @param skuId
     * @return
     */
    ProductDetailDTO getItemBySkuId(Long skuId);
}
java
/*
     该类封装了商品详情页所需的所有数据
 */
@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;
}
3.1 获取SKU及其图片列表 &#8203;

在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<SkuImage>
xml
    <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>
3.2 获取SKU所属的SPU销售属性集合 &#8203;

在SkuService中我们实现如下方法

java
List<SpuSaleAttributeInfoDTO> getSpuSaleAttrListCheckBySku(Long skuId, Long spuId);

当点击某一个SKU商品,就会进入到商品详情页,在商品详情页中,可以显示该SKU商品对应的SPU中,所有销售属性值。这里,关于销售属性值,我们需要注意的是

  • 我们需要获取当前SKU所属的SPU中,包含的所有销售属性和销售属性值
  • 我们还要知道,当前浏览的这个SKU商品对应的销售属性值,并将其默认选中

如何实现呢?其实我们在查询销售属性值的时候,利用左外连接,就可以很快找到当前sku商品对应的所有销售属性值,并对其加以标记

  • 首先,将spu_sale_attr_info与spu_sale_attr_value两张表做内连接, 其结果就包含指定了SPU所有销售属性及销售属性对应的销售属性值
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
       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商品的每个销售属性,以及销售属性值了
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
        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,修改连接方式为左外连接,并且增加连接条件
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如下:

xml
       <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

3.3 不同销售属性值组合 &#8203;

我们需要实现定义在SpuService接口中的方法

Map<String, Long> getSkuValueIdsMap(Long spuId);

当点击某一个SKU商品,就会进入到商品详情页,在商品详情页中,可以显示该SKU商品对应的SPU中,所有销售属性值。这里,关于销售属性值,我们还需要注意,我们也可以在页面中,修改选中的销售属性值组合,从而跳转到不同的SKU商品详情页

所以,随之而来的问题就是,SKU商品所属的SPU销售属性值有多种不同的组合,而不同的SPU销售属性组合对应不同的SKU商品,我们如何让前端快速确定,什么销售属性组合对应那个SKU商品从而实现商品详情页的跳转呢?

  • 首先,我们可以先获取,SPU中每一个SKU商品对应的所有销售属性值
sql
       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表做连接查询,过滤下架商品
sql
        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如下:
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:

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);
sql
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/group_concat sku查询示例.png)

完整的sql定义以及结果映射如下:

xml
    <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>
3.4 获取SKU平台属性集合 &#8203;

在SkuService中实现如下方法

java
 List<PlatformAttributeInfoDTO> getPlatformAttrInfoBySku(Long skuId);
xml
    <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>
3.5 三级类目 &#8203;

在skuinfo表中,针对每一个SKU商品,我们都记录了其对应的第三级类目的id,所以我们就可以顺藤摸瓜,查询出该SKU商品所属的第一,二级目录,这样我们就可以查询出,包含该SKU商品的三级目录信息了。为此,我们需要实现CategoryService中以下接口方法

java
    /*
        获取和该三级类目相关完整信息(包括它所属的一级类目和二级类目)
     */
    CategoryHierarchyDTO getCategoryViewByCategoryId(Long thirdLevelCategoryId);
java
@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;
}

这里我们可以定义定义多表查询

xml
<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>

4 总代码流程梳理 &#8203;

java
    @Autowired
    SpuService spuService;

    @Autowired
    SkuService skuService;

    @Autowired
    CategoryService categoryService;


    @Autowired
    SearchApiClient searchApiClient;

    @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<Spu> 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;
    }

笔记 &#8203;

  • 左外连接是在内连接的基础上和左表并集