管理主页分类

拖动分类调整顺序,并选择是否在主页显示。设置仅保存在当前浏览器。

  • 微服务课件 31
  • Java 基础 28
  • Java Web 开发 25
  • 多来买 18
  • LeetCode 题解 8
  • 开发工具 8
  • 网络工具 2
  • 黑马头条 2
  • Java 记忆恢复 1

多来买:商品详情数据模型与聚合实战

2409 字
12 分钟
多来买:商品详情数据模型与聚合实战

多来买:商品详情数据模型与聚合实战#

事实来源:当前 product-service、product-api 的 Controller、Service、DTO、Mapper 与模型源码
证据状态:代码已确认,未构建、未启动、未连接数据库。
阅读目标:理解详情页为什么需要聚合 SPU、SKU、图片、分类和属性,而不是返回一张商品表。

1. 业务问题与边界#

商品详情接口接收一个 skuId,最终却要返回价格、图片、分类面包屑、海报、销售属性、规格切换关系和平台属性。

这些数据分布在多张表中,且部分查询必须先拿到 SKU 才能确定 spuId 或三级分类 ID。

本文只回答三个问题:

  1. 商品详情读模型由哪些数据组成;
  2. Controller 到 Mapper 的聚合链路怎样工作;
  3. 当前查询结构有哪些明确边界。

Bloom、缓存和异步编排见下一篇,Elasticsearch 与上下架同步分别见本专题第 3、4 篇。

2. 模块职责#

模块本文职责
duolaimall-product/product-api定义详情 DTO 和跨服务商品 DTO
duolaimall-product/product-service提供详情入口、聚合 Service 和 Mapper
MySQL 商品库保存 SKU、SPU、图片、分类和属性事实数据
MapStruct Converter在 PO、DTO 和详情读模型之间转换

详情链路没有跨到搜索服务:详情读取商品数据库,搜索服务维护的是另一套面向检索的读模型。

3. SPU、SKU 与两类属性#

概念示例当前用途
SPU某一款手机型号聚合一组可切换 SKU
SKU某颜色和存储组合展示、定价、上下架的最小单元
销售属性颜色、版本详情页规格选择和 SKU 切换
平台属性屏幕尺寸、内存规格展示与搜索筛选

主要数据表:

作用
sku_infoSKU 基础信息、SPU、价格、品牌、分类和上架状态
sku_imagespu_imageSKU 图片关联及图片内容
spu_posterSPU 详情海报
spu_sale_attr_infospu_sale_attr_valueSPU 可选销售属性和值
sku_sale_attr_valueSKU 实际选择的销售属性值
sku_platform_attr_valueSKU 关联的平台属性和值
platform_attr_infoplatform_attr_value平台属性定义及候选值
一、二、三级分类表分类面包屑

4. 端到端链路#

sequenceDiagram participant C as 客户端 participant PC as ProductDetailController participant PD as ProductDetailServiceImpl participant SKU as SkuService participant SPU as SpuService participant CAT as CategoryService participant DB as MySQL C->>PC: GET /goods/{skuId} PC->>PD: getItemBySkuIdAsync(skuId) PD->>SKU: getSkuInfo(skuId) SKU->>DB: sku_info + 图片关联 DB-->>SKU: SkuInfoDTO par 独立查询 PD->>SKU: getSkuPrice(skuId) PD->>SKU: getPlatformAttrInfoBySku(skuId) and 依赖 SKU 根数据 PD->>CAT: 查询分类层级 PD->>SPU: 查询海报和规格映射 PD->>SKU: 查询销售属性选中态 end PD-->>PC: ProductDetailDTO PC-->>C: Result.ok(detail)

图中只表达依赖关系;具体并发编排在本专题第 2 篇展开。

5. 详情读模型#

源码:duolaimall-product/product-api/src/main/java/com/cskaoyan/mall/product/dto/ProductDetailDTO.javaProductDetailDTO

@Data
public class ProductDetailDTO {
SkuInfoDTO skuInfo;
List<SpuSaleAttributeInfoDTO> spuSaleAttrList;
String valuesSkuJson;
BigDecimal price;
CategoryHierarchyDTO categoryHierarchy;
List<SpuPosterDTO> spuPosterList;
List<SkuSpecification> skuAttrList;
}

它是面向详情页面的读模型,不对应单张表。

DTO 字段查询入口根依赖
skuInfoSkuService#getSkuInfoskuId
priceSkuService#getSkuPriceskuId
categoryHierarchyCategoryService#getCategoryViewByCategoryIdSKU 的三级分类 ID
spuPosterListSpuService#findSpuPosterBySpuIdSKU 的 spuId
spuSaleAttrListSkuService#getSpuSaleAttrListCheckBySkuspuIdskuId
valuesSkuJsonSpuService#getSkuValueIdsMapspuId
skuAttrListSkuService#getPlatformAttrInfoBySkuskuId

6. 当前 Controller 入口#

源码:duolaimall-product/product-service/src/main/java/com/cskaoyan/mall/product/controller/ProductDetailController.javaProductDetailController#goodsDetail

@GetMapping("/goods/{skuId}")
public Result goodsDetail(@PathVariable Long skuId) {
ProductDetailDTO productDetailDTO =
productDetailService.getItemBySkuIdAsync(skuId);
return Result.ok(productDetailDTO);
}

类中保留了 V1、V2 和其他异步版本,但当前 Controller 调用的是 getItemBySkuIdAsync

判断当前行为时应沿这个方法继续阅读,不能把保留方法当成运行入口。

Controller 没有拆解子查询,也没有对 skuId 做业务校验;聚合和异常都交给后续层。

7. SKU 基础信息与图片#

源码:duolaimall-product/product-service/src/main/java/com/cskaoyan/mall/product/service/impl/SkuServiceImpl.javaSkuServiceImpl#getSkuInfo

@Override
@CsmallCache(prefix = "skuInfo:")
public SkuInfoDTO getSkuInfo(Long skuId) {
SkuInfo skuInfo = skuInfoMapper.selectById(skuId);
LambdaQueryWrapper<SkuImage> queryWrapper =
new LambdaQueryWrapper<>();
queryWrapper.eq(SkuImage::getSkuId, skuId);
List<SkuImage> skuImageList =
skuImageMapper.selectList(queryWrapper);
skuImageList.forEach(skuImage -> {
SpuImage spuImage =
spuImageMapper.selectById(skuImage.getSpuImgId());
skuImage.setImgName(spuImage.getImgName());
skuImage.setImgUrl(spuImage.getImgUrl());
});
skuInfo.setSkuImageList(skuImageList);
return skuInfoConverter.skuInfoPO2DTO(skuInfo);
}

数据流:

sku_info
-> sku_image 列表
-> 每条关联再查 spu_image
-> 回填 SkuInfo
-> MapStruct 转为 SkuInfoDTO

当前代码具有逐图片查询特征。缓存命中可以减少重复回源,但首次查询仍可能出现一加 N 次访问。

selectById 返回 null,后续 setSkuImageList 无法正常执行;前置 Bloom 只能降低无效请求,不能替代空值处理。

8. 分类面包屑#

源码:duolaimall-product/product-service/src/main/resources/com/cskaoyan/mall/product/mapper/ThirdLevelCategoryMapper.xmlselectCategoryHierarchyDTOById

<select id="selectCategoryHierarchyDTOById"
resultType="com.cskaoyan.mall.product.model.CategoryHierarchy">
select
flc.id AS firstLevelCategoryId,
flc.name AS firstLevelCategoryName,
slc.id AS secondLevelCategoryId,
slc.name AS secondLevelCategoryName,
tlc.id AS thirdLevelCategoryId,
tlc.name AS thirdLevelCategoryName
from third_level_category tlc
inner join second_level_category slc
on tlc.second_level_category_id = slc.id
inner join first_level_category flc
on slc.first_level_category_id = flc.id
where tlc.id = #{thirdLevelCategoryId}
and tlc.is_deleted = 0
and slc.is_deleted = 0
and flc.is_deleted = 0
</select>

这里通过一次联表获得三级完整路径,而不是在 Java 中逐级查询三次。

查询还要求三个分类层级都未逻辑删除,因此任一上级无效都可能导致整个面包屑为空。

9. 销售属性与选中态#

源码:duolaimall-product/product-service/src/main/resources/com/cskaoyan/mall/product/mapper/SpuSaleAttrInfoMapper.xmlselectSpuSaleAttrListCheckedBySku

<select id="selectSpuSaleAttrListCheckedBySku"
resultMap="spuSaleAttrMap">
select a.id,
a.spu_id,
a.sale_attr_id,
a.sale_attr_name,
b.id as bid,
b.spu_sale_attr_value_name,
IF(c.id is NULL, 0, 1) as is_checked
from spu_sale_attr_info a
inner join spu_sale_attr_value b
on a.id = b.spu_sale_attr_id
left join sku_sale_attr_value c
on b.id = c.spu_sale_attr_value_id
and c.sku_id = #{skuId}
where a.spu_id = #{spuId}
order by a.sale_attr_id, b.id
</select>

查询先返回 SPU 的全部销售属性和值,再通过 LEFT JOIN 标记当前 SKU 是否选中。

因此前端同时拥有:

  • 可以展示的完整选项;
  • 当前 SKU 的选中状态;
  • 切换规格时需要拼接的候选值。

10. 规格组合到 SKU 的映射#

源码:duolaimall-product/product-service/src/main/resources/com/cskaoyan/mall/product/mapper/SkuSaleAttrValueMapper.xmlselectSaleAttrValuesBySpu

<select id="selectSaleAttrValuesBySpu"
resultType="com.cskaoyan.mall.product.model.SkuSaleAttributeValuePermutation">
select a.sku_id as skuId,
group_concat(
a.spu_sale_attr_value_id
order by c.sale_attr_id separator '|'
) as SkuSaleAttrValuePermutation
from sku_sale_attr_value a
inner join spu_sale_attr_value b
on b.id = a.spu_sale_attr_value_id
and a.is_deleted = 0
and b.is_deleted = 0
inner join spu_sale_attr_info c
on c.id = b.spu_sale_attr_id
and c.is_deleted = 0
inner join sku_info d
on d.id = a.sku_id
and d.is_sale = 1
and d.is_deleted = 0
where a.spu_id = #{spuId}
group by a.sku_id
</select>

Service 把结果转换为:

"销售属性值ID|销售属性值ID" -> skuId

属性值必须按稳定顺序拼接,否则相同选择可能生成不同 key。

SQL 只包含已上架、未删除 SKU;但查询结果会被缓存,上下架后是否立即更新取决于缓存失效,当前未看到统一失效链路。

11. 平台属性聚合#

源码:duolaimall-product/product-service/src/main/java/com/cskaoyan/mall/product/service/impl/SkuServiceImpl.javaSkuServiceImpl#getPlatformAttrInfoBySku

List<SkuPlatformAttributeValue> relations =
skuPlatformAttrValueMapper.selectList(queryWrapper);
List<PlatformAttributeInfo> infos = relations.stream()
.map(relation -> {
PlatformAttributeInfo info =
platformAttrInfoMapper.selectById(relation.getAttrId());
PlatformAttributeValue value =
platformAttrValueMapper.selectById(relation.getValueId());
info.setAttrValueList(Arrays.asList(value));
return info;
})
.collect(Collectors.toList());

该分支先查 SKU 属性关联,再为每个关联分别查询属性定义和值。

若 SKU 有 N 个平台属性,回源结构近似:

1 次关联查询 + N 次属性定义查询 + N 次属性值查询

异步化只能与其他详情分支并行,不能消除本分支内部的 N+1 查询。

12. 数据依赖图#

flowchart TD ID["skuId"] --> SKU["SKU 基础信息"] ID --> PRICE["实时价格"] ID --> SPEC["平台属性"] SKU --> SPU["spuId"] SKU --> CATID["三级分类 ID"] SPU --> POSTER["SPU 海报"] SPU --> SALE["销售属性与选中态"] SPU --> MAP["属性组合到 SKU 映射"] CATID --> CAT["完整分类面包屑"] SKU --> DTO["ProductDetailDTO"] PRICE --> DTO SPEC --> DTO POSTER --> DTO SALE --> DTO MAP --> DTO CAT --> DTO

根节点不是 SPU,而是 skuId 对应的 SKU 基本信息;多个后续分支从中取得 spuId 和分类 ID。

13. 正常、空值与失败分支#

场景当前静态结果
SKU 和关联数据完整聚合为 ProductDetailDTO
图片关联为空SKU 仍可返回,图片列表为空
SKU 不存在getSkuInfo 存在空指针触发路径
分类任一级已删除联表结果可能为空
属性关联存在但定义缺失逐项查询可能返回空并影响回填
数据库某分支异常是否影响整页取决于异步分支异常策略

这些结果来自代码结构,不是运行观测。

14. 已确认限制与候选风险#

14.1 已确认限制#

  • 详情是多表聚合,不具备单表读取的原子快照。
  • 图片和平台属性存在逐项查询结构。
  • Controller 没有对非法或不存在的 skuId 做显式校验。
  • 多个历史详情实现仍在类中,当前入口只使用异步版本。
  • 规格组合缓存没有看到与上下架联动的统一失效。

14.2 候选风险#

风险触发机制缺失证据
无效 SKU 空指针Bloom 误判后数据库仍返回空未运行接口复现
冷查询放大数据库访问图片与平台属性 N+1未做 SQL 统计或压测
聚合字段短暂不一致多分支独立读取不同表未构造并发写入场景
规格切换仍包含旧映射缓存失效链路不明确未连接 Redis 验证

15. 如果重做(非当前实现)#

可评估方向:

  1. 对 SKU 不存在建立明确的业务结果,而不是依赖空对象继续聚合;
  2. 使用批量查询或联表减少图片、平台属性的 N+1;
  3. 明确详情缓存与上下架、属性更新之间的失效事件;
  4. 为聚合分支定义必需字段和可降级字段;
  5. 使用统一线程池和超时策略编排独立查询。

这些是演进建议,不代表当前项目已经实现。

16. 关键源码导航#

阅读问题项目相对路径类/方法
详情 HTTP 入口duolaimall-product/product-service/src/main/java/com/cskaoyan/mall/product/controller/ProductDetailController.javagoodsDetail
详情返回模型duolaimall-product/product-api/src/main/java/com/cskaoyan/mall/product/dto/ProductDetailDTO.javaProductDetailDTO
当前聚合实现duolaimall-product/product-service/src/main/java/com/cskaoyan/mall/product/service/impl/ProductDetailServiceImpl.javagetItemBySkuIdAsync
SKU 与图片duolaimall-product/product-service/src/main/java/com/cskaoyan/mall/product/service/impl/SkuServiceImpl.javagetSkuInfo
分类联表duolaimall-product/product-service/src/main/resources/com/cskaoyan/mall/product/mapper/ThirdLevelCategoryMapper.xmlselectCategoryHierarchyDTOById
销售属性选中态duolaimall-product/product-service/src/main/resources/com/cskaoyan/mall/product/mapper/SpuSaleAttrInfoMapper.xmlselectSpuSaleAttrListCheckedBySku
规格组合duolaimall-product/product-service/src/main/resources/com/cskaoyan/mall/product/mapper/SkuSaleAttrValueMapper.xmlselectSaleAttrValuesBySpu

17. 短复习点#

  1. 详情页返回的是跨表读模型,skuId 是聚合根入口。
  2. SKU 先提供 spuId 和三级分类 ID,再触发依赖查询。
  3. 销售属性负责规格选择,平台属性负责规格展示与搜索筛选。
  4. 图片和平台属性当前存在 N+1 回源结构。
  5. 本文只能确认代码链路,不能声称数据库与缓存当前可运行。

18. 一句话总结#

多来买商品详情以 SKU 为根,把 SPU、图片、分类和两类属性聚合成页面读模型;实现能够串起完整数据链路,但空值处理、N+1 查询和缓存失效仍是必须如实说明的边界。

文章分享

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

多来买:商品详情数据模型与聚合实战
https://firefly-mu-weld.vercel.app/posts/duolaimai-product-detail-aggregation/
作者
Daisy
发布于
2026-08-03
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
Daisy
Hello, I'm Daisy.
公告
欢迎来到我的博客!这是一则示例公告。
分类
标签

文章目录