平台属性与品牌管理

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

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

平台属性和平台属性值

平台属性和平台属性值主要用于商品的搜索,平台属性是属于商品类目的,不同的商品类目具有不同的平台属性。比如,牛仔裤这个商品类目就有尺码,腰型等平台属性,而手机这个商品类目就具有机身内存,运行内存,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,以及其售价和库存。
商品服务
工程结构




分类信息查询
// 查询一级分类@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);@Datapublic 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);@Datapublic 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怎么来呢?

其实,当我们在前端选择logo图片上传的时候,前端就会向后端发起请求,从而将图片保存到minio,并在响应中获取保存图片的url,因此我们接收到的保存品牌的参数中,包含的是品牌logo的url。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!