平台属性与品牌管理

3522 字
18 分钟
平台属性与品牌管理

业务知识#

商品类目#

商城中的商品都是有其所属类目的。在我们的商城中,商品的类目分成了三级,包括一级类目,二级类目,三级类目

比如,手机/运营商/数码就是一级类目,手机通讯就是二级类目,手机就是三级类目

平台属性和平台属性值#

平台属性和平台属性值主要用于商品的搜索,平台属性是属于商品类目的,不同的商品类目具有不同的平台属性。比如,牛仔裤这个商品类目就有尺码,腰型等平台属性,而手机这个商品类目就具有机身内存,运行内存,CPU型号等平台属性。

不同的层级的类目也都可能具有平台属性,但是在我们的项目中平台属性属于三级类目

SPU 与 SKU#

SPU(Standard Product Unit):标准化产品单元。是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。通俗点讲,属性值、特性相同的商品就可以称为一个SPU。

换言之,SPU通常表示的是具有相同特征的商品集合,而不是某一个具体的商品,比如 荣耀V30 PRO手机就是一个SPU,所有的荣耀V30 PRO手机这有这些相同的特征:它们都是双模5G手机通用芯片,使用麒麟990处理器,机身尺寸和重量相同等等。但是荣耀V30 PRO手机是一个具体的商品吗?因为不同的荣耀V30 PRO手机还具有不同的颜色(比如,冰岛幻境,幻夜星河等等),还有不同的版本(比如,8G + 256G,8G + 128G)

SKU(Stock Keeping Unit)库存量单位,即库存进出计量的单位, 可以是以件、盒、托盘等为单位。SKU是物理上不可分割的最小存货单元。

和SPU不同,一个SKU可以表示一个具体的商品,比如荣耀V30 PRO幻夜星河 8+128GB。我们在电商网站中检索,在详情页中查看,以及最终下单购买的都是SKU商品

销售属性与销售属性值#

是组成SKU的特殊属性,一旦我们确定了商品的SPU,在结合SPU的不同销售属性取值,就能得到不同的SKU。

因为商品的价格和商品的库存都是针对商品SKU的,不同的销售属性取值确定的是不同的商品SKU,以及其售价和库存。

商品服务#

工程结构#

![](/assets/firefly-docs/microservice/microservice-10-platform-attr-brand/duolaimall-common structure.png)

![](/assets/firefly-docs/microservice/microservice-10-platform-attr-brand/duolaimall-gateway init.png)

![](/assets/firefly-docs/microservice/microservice-10-platform-attr-brand/product-service structure.png)

分类信息查询#

// 查询一级分类
@GetMapping("admin/product/getCategory1")
public Result<List<FirstLevelCategoryDTO>> getCategory1(){
}
// 根据一级分类查询二级分类
@GetMapping("/admin/product/getCategory2/{firstLevelCategoryId}")
public Result<List<SecondLevelCategoryDTO>> getCategory2(@PathVariable Long firstLevelCategoryId){
}
// 根据二级分类,查询三级分类
@GetMapping("/admin/product/getCategory3/{category2Id}")
public Result<List<ThirdLevelCategoryDTO>> getCategory3(@PathVariable Long category2Id){
}
/**
* 查询所有的一级分类信息
* @return
*/
List<FirstLevelCategoryDTO> getFirstLevelCategory();
/**
* 根据一级分类Id 查询二级分类数据
*/
List<SecondLevelCategoryDTO> getSecondLevelCategory(Long firstLevelCategoryId);
/**
* 根据二级分类Id 查询三级分类数据
*/
List<ThirdLevelCategoryDTO> getThirdLevelCategory(Long secondLevelCategoryId);

以上三个接口定义在CategoryService中,是对三张商品类目表的单表查询,非常简单。

平台属性的查询#

// 根据分类Id查询平台属性以及平台属性值
// http://localhost/admin/product/attrInfoList/3/20/149
@GetMapping("/admin/product/attrInfoList/{firstLevelCategoryId}/{secondLevelCategoryId}/{thirdLevelCategoryId}")
public Result getAttrInfoList(@PathVariable Long firstLevelCategoryId,
@PathVariable Long secondLevelCategoryId,
@PathVariable Long thirdLevelCategoryId){
}

要实现三级类目下平台属性的查询,必须实现以下定义在CategoryService中的方法

/**
* 根据分类Id 获取平台属性数据
* 接口说明:
* 1,平台属性可以挂在一级分类、二级分类和三级分类
* 2,查询一级分类下面的平台属性,传:firstlevelCatogoryId,0,0; 取出该分类的平台属性
* 3,查询二级分类下面的平台属性,传:firstlevelCatogoryId,category2Id,0;
* 取出对应一级分类下面的平台属性与二级分类对应的平台属性
* 4,查询三级分类下面的平台属性,传:firstlevelCatogoryId,category2Id,category3Id;
* 取出对应一级分类、二级分类与三级分类对应的平台属性
*/
List<PlatformAttributeInfoDTO> getPlatformAttrInfoList(Long firstLevelCategoryId, Long secondLevelCategoryId, Long thirdLevelCategoryId);
@Data
public class PlatformAttributeInfoDTO {
@ApiModelProperty(value = "平台属性id")
private Long id;
@ApiModelProperty(value = "属性名称")
private String attrName;
@ApiModelProperty(value = "分类id")
@TableField("category_id")
private Long categoryId;
@ApiModelProperty(value = "分类层级")
private Integer categoryLevel;
/*
平台属性值集合,这里注意一个平台属性,有多个属性取值
*/
private List<PlatformAttributeValueDTO> attrValueList;
}

这里要注意的是,返回的PlatformAttributeInfoDTO包含完整的平台属性信息,即既包含平台属性名又包含平台属性值,所以在查询数据库中的平台属性时,就不可避免的涉及到 platform_attr_info(平台属性) 和 platform_attr_value(平台属性值表)的连表查询,查询的SQL语句如下:

<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>
<!--根据分类id查询平台属性集合-->
<select id="selectPlatFormAttrInfoList" 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
<where>
<if test="firstLevelCategoryId !=null and firstLevelCategoryId !=0">
or ( pai.category_level=1 and pai.category_id=#{firstLevelCategoryId})
</if>
<if test="secondLevelCategoryId !=null and secondLevelCategoryId !=0">
or (pai.category_level=2 and pai.category_id=#{secondLevelCategoryId})
</if>
<if test="thirdLevelCategoryId !=null and thirdLevelCategoryId !=0">
or (pai.category_level=3 and pai.category_id=#{thirdLevelCategoryId})
</if>
</where>
order by pai.category_level ,pai.id
</select>

平台属性的添加#

// 保存平台属性
// http://localhost/admin/product/saveAttrInfo
@PostMapping("/admin/product/saveAttrInfo")
public Result saveAttrInfo(@RequestBody PlatformAttributeParam platformAttributeParam) {
}

要实现给三级类目添加平台属性以及属性值的功能,必须实现以下定义在PlatformAttributeService中的接口方法

void savePlatformAttrInfo(PlatformAttributeParam platformAttributeParam);
@Data
public class PlatformAttributeParam {
private Long id;
private String attrName;
private Long categoryId;
private Integer categoryLevel;
/*
平台属性值集合,这里注意一个平台属性,有多个属性取值
*/
private List<PlatformAttributeValueParam> attrValueList;
}

在向数据库中保存一条平台属性信息信息的时候,有一点需要格外注意:

  • 无论是新增,还是修改一条平台属性(包括修改其平台属性值),我们都是跳转到平台属性的页面,添加或者修改数据,最后点击保存,发送保存平台属性信息的请求
  • 但是,后端需要区分到底是新增,还是修改平台属性,因为这涉及到在数据库中,到底是插入还是更新的问题
  • 对于平台属性而言,如果是修改的话,那么前端的保存请求会携带被修改的平台属性的id,如果是新增的话则不会,所以基于平台属性id是否为null,我们可以判断对于平台属性究竟是添加还是更新
  • 但是,一条平台属性包含多个属性值,可以通过判断前端传递的参数中每个平台属性值id对否为null确定是平台属性值的新增或更新,但是比较麻烦,所以我们换种思路,对于平台属性值,我们不做判断,先删除数据库中可能已经存在的目标平台属性的属性值,然后将前端传递的参数中包含的平台属性值全部插入数据库
  • 同时,因为对于平台属性值不区分新增或修改,统一先删除在插入,如果前端而言如果是新增的话,在平台属性插入数据库前,前端是不知道平台属性id的,因此,在处理完平台属性后,在插入平台属性值之前,我们需要统一给每一个平台属性值,设置其对应的平台属性id,即attrId属性值,然后再将其插入数据库

保存平台属性及其属性值方法的核心逻辑如下:

// 将前端参数转化为
PlatformAttributeInfo platformAttributeInfo
= platformAttributeInfoParamConverter.attributeInfoParam2Info(platformAttributeParam);
// 判断平台属性
if (platformAttributeInfo.getId() != null) {
// 修改数据
platformAttrInfoMapper.updateById(platformAttributeInfo);
} else {
// 新增
platformAttrInfoMapper.insert(platformAttributeInfo);
}
// platformAttrValue平台属性值,先删除,在新增的方式!
LambdaQueryWrapper<PlatformAttributeValue> platformAttributeValueQueryWrapper = new LambdaQueryWrapper<>();
// 删除平台属性原本在数据库中对应的属性值
platformAttributeValueQueryWrapper.eq(PlatformAttributeValue::getAttrId, platformAttributeInfo.getId());
platformAttrValueMapper.delete(platformAttributeValueQueryWrapper);
// 获取页面传递过来的所有平台属性值数据
List<PlatformAttributeValue> attrValueList = platformAttributeInfo.getAttrValueList();
if (!CollectionUtils.isEmpty(attrValueList)) {
// 循环遍历
for (PlatformAttributeValue platformAttributeValue : attrValueList) {
// 获取平台属性Id 给attrId
platformAttributeValue.setAttrId(platformAttributeInfo.getId());
platformAttrValueMapper.insert(platformAttributeValue);
}
}

平台属性的回显值#

// http://localhost/admin/product/getAttrValueList/106
// 平台属性值回显
@GetMapping("/admin/product/getAttrValueList/{attrId}")
public Result<List<PlatformAttributeValueDTO>> getAttrInfoDTO(@PathVariable Long attrId) {
}

当我们在某一个平台属性上点击修改的时候,会跳转到平台属性对应的界面,显示当前平台属性以及属性值信息。为了实现这一功能,我们必须实现定义在PlatformAttributeService中的如下接口方法:

List<PlatformAttributeValueDTO> getPlatformAttrInfo(Long attrId);

该方法的实现就比较简单了,根据平台属性id查询其对应的属性值列表即可。

图片上传#

在我们的电商网站中,不论是品牌logo,还是商品图片,商品的海报,都是一些需要保存的图片数据,这些图片数据都是需要给用户展示的,所以我们必须在后台完成全部的图片上传工作。

既然要上传图片,随之而来的问题就是图片存储在哪里?和上一个项目类似,我们仍然采用OSS的方式 来存储,但稍有不同的是,我们不在使用阿里云,而是本地的对象存储服务器MinIO来存储。

MinIO介绍#

MinIO 是一个基于Apache License v2.0开源协议的对象存储服务器。它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件等,而一个对象文件可以是任意大小,从几kb到最大5T[不等]。详情参见官方文档地址

MinIO作为一个优秀的开源对象存储服务器具有如下特征:

  • 高性能:作为高性能对象存储,在标准硬件条件下它能达到55GB/s的读、35GB/s的写速率
  • 可扩容:不同MinIO集群可以组成联邦,并形成一个全局的命名空间,并跨越多个数据中心
  • SDK支持: 它有类似Java、Python或Go等语言的sdk支持
  • 支持纠删码: MinIO使用纠删码、Checksum来防止硬件错误和静默数据污染。在最高冗余度配置下,即使丢失1/2的磁盘也能恢复数据。
  • 有控制台界面
  • 功能简单: 这一设计原则让MinIO不容易出错、更快启动

安装#

在这里我们使用Docker来安装,参考环境搭建文档。

MinIO Server启动启动后,我们可以通过浏览器访问控制台界面,但是需要登录,登录时默认的用户名是admin,密码是admin123456

要是用MinIo我们还必须了解两个基本概念:

  • 对象: 在MinIO中对象指的是二进制数据,甚至有时指的是Blob(Binary Large OBject),二进制数据可以是图片,音视频文件,可执行文件等等
  • 桶(bucket): MinIO用桶来组织对象,一个桶类似于操作系统中的一个目录,一个桶中可以存储任意多个对象

所以,要是用MinIO必须首先创建桶,向桶中存取对象,同时还要注意,如果要想访问到桶中的数据,我们得把桶的权限设置为public

MinIO的Java客户端#

我们还可以使用MinIO提供的Java语言客户端,通过Java语言操作MinIO实现文件上传。

在工程中导入MinIO客户端依赖如下

<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.2.0</version>
</dependency>

基于客户端依赖,即可以实现文件上传功能,我们定义FilepuloadController代码如下:

@RestController
@RequestMapping("admin/product")
public class FileUploadController {
// 获取文件上传对应的地址
@Value("${minio.endpointUrl}")
public String endpointUrl;
@Value("${minio.accessKey}")
public String accessKey;
@Value("${minio.secreKey}")
public String secreKey;
@Value("${minio.bucketName}")
public String bucketName;
// 文件上传控制器
@PostMapping("fileUpload")
public Result fileUpload(MultipartFile file) throws Exception{
// 准备获取到上传的文件路径!
String url = "";
// 使用MinIO服务的URL,端口,Access key和Secret key创建一个MinioClient对象
MinioClient minioClient =
MinioClient.builder()
.endpoint(endpointUrl)
.credentials(accessKey, secreKey)
.build();
// 检查存储桶是否已经存在
boolean isExist = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if(isExist) {
System.out.println("Bucket already exists.");
} else {
// 创建一个名为asiatrip的存储桶,用于存储照片的zip文件。
minioClient.makeBucket(MakeBucketArgs.builder()
.bucket(bucketName)
.build());
}
// 定义一个文件的名称 : 文件上传的时候,名称不能重复!
String fileName = System.currentTimeMillis()+ UUID.randomUUID().toString();
// 使用putObject上传一个文件到存储桶中。
minioClient.putObject(
PutObjectArgs.builder().bucket(bucketName).object(fileName).stream(
file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build());
url = endpointUrl+"/"+bucketName+"/"+fileName;
System.out.println("url:\t"+url);
// 将文件上传之后的路径返回给页面!
return Result.ok(url);
}
}

品牌管理#

业务分析#

在我们的电商网站中,还涉及到了品牌数据的管理

关于品牌管理,我们需要实现品牌的增删改查功能

代码实现#

// http://localhost/admin/product/baseTrademark/1/10
// 查看品牌列表
@GetMapping("/admin/product/baseTrademark/{pageNo}/{pageSize}")
public Result<TrademarkPageDTO> getTradeMarkDTOList(@PathVariable Long pageNo, @PathVariable Long pageSize) {
}
// 保存品牌
//http://localhost/admin/product/baseTrademark/save
@PostMapping("/admin/product/baseTrademark/save")
public Result save(@RequestBody TrademarkParam trademarkParam){
}
// http://localhost/admin/product/baseTrademark/remove/10
// 删除品牌
@DeleteMapping("/admin/product/baseTrademark/remove/{tradeMarkId}")
public Result deleteById(@PathVariable Long tradeMarkId){
}
// http://localhost/admin/product/baseTrademark/get/17
// 查询品牌
@GetMapping("/admin/product/baseTrademark/get/{tradeMarkId}")
public Result<TrademarkDTO> getTradeMarkDTO(@PathVariable Long tradeMarkId) {
}
// 修改品牌
// http://localhost/admin/product/baseTrademark/update
@PutMapping("/admin/product/baseTrademark/update")
public Result updateTradeMark(@RequestBody TrademarkParam trademarkParam){
}

其中查询又分成了根据id查询和分页查询(根据id查询的效果,主要用于修改品牌信息时数据的回显),对应TrademarkService中的5个方法

public interface TrademarkService {
/*
根据品牌id,查询品牌
*/
TrademarkDTO getTrademarkByTmId(Long tmId);
/**
* 根据分页参数,分页查询品牌列表
*/
TrademarkPageDTO getPage(Page<Trademark> pageParam);
/*
保存品牌
*/
void save(TrademarkParam trademarkParam);
/*
更新品牌
*/
void updateById(TrademarkParam trademarkParam);
/*
删除品牌
*/
void removeById(Long id);
}

其中,我们需要注意的一点是,在新增品牌的时候,我们是需要存储品牌logo的图片的url的,这个url怎么来呢?

![](/assets/firefly-docs/microservice/microservice-10-platform-attr-brand/商品图片logo url.png)

其实,当我们在前端选择logo图片上传的时候,前端就会向后端发起请求,从而将图片保存到minio,并在响应中获取保存图片的url,因此我们接收到的保存品牌的参数中,包含的是品牌logo的url。

文章分享

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

平台属性与品牌管理
https://firefly-mu-weld.vercel.app/posts/microservice-10-platform-attr-brand/
作者
Daisy
发布于
2026-06-14
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
Daisy
Hello, I'm Daisy.
公告
欢迎来到我的博客!这是一则示例公告。
分类
标签

文章目录