目录
类别管理--前台
商品类别表用来管理后台的商品类目, 通过建立商品类别之间父级和子级的层级关系, 为商品前台构建类目导航所需的类目树, 方便类目树的开展并帮助前台的用户快速查找想要购买商品的商品类别.
当用户确定手机-->智能手机的类别后, 用户还可以通过品牌或其他属性进行筛选, 因此品牌表也需要和商品类别表进行关联.
通过将商品类别和属性模板进行关联, 一旦确定好商品所属类别, 该商品能够选择的模板数量就会变得非常有限.
开发分类功能
数据导入
在给大家提供的项目的doc文件夹下的sql文件夹中
有多个sql语句文件
分别去运行它们,我们可以可以获得前台的数据库信息了
分类功能实现逻辑
- 先从数据库中查询出所有分类信息,一次性全查
- 将查询出的分类信息保存在Redis,以备后续用户直接获取
- 如果是第一次访问,我们就要将所有分类级别按照层级关联关系,进行关联,最后返回包含层级关联关系的分类列表
业务分析
查询返回分类列表实际上最终得到的是一个具有分类关联结构的集合
下面简单描述一下它的数据结构
[
{id:1,name:"手机/运行商/数码",parentId:0,depth:1,children:[
{id:2,name:"手机通讯",parentId:1,depth:2,children:[
{id:3,name:"智能手机",parentId:2,depth:3,children:null},
{id:4,name:"非智能手机",parentId:2,depth:3,children:null}
]
]},
{id:5,name:"电脑/办公",parentId:0,depth:1,children:[....]}
]
上面是我们需要获得的对象的结构
可以理解为下图
数据库中格列的含义
数据库中分类数据的列的含义基本解释
id:主键
name:显示在页面上的分类名称
parentId:父分类的id 如果是一级分类父分类id为0
depth:分类深度,当前项目就是3级分类,1\2\3 分别代表它的等级
keyword:搜索关键字,各关键字使用英文的逗号分隔.
sort:排序依据--自定义排序序号 正常查询时,根据此列进行排序,数字越小越出现在前面(升序)
icon:图标地址
enable:是否可用, 1表示启用
isparent:是否为父分类 0 假 1真
isdisplay:是否显示在导航栏 0不显示 1显示
E-R图
- category表与brand_category表关联, 负责前台商品筛选和后台商品发布, 用户先选择商品类别, 再选择当前商品类别下某个具体的品牌, 从而浏览该品牌下的所有商品.使用brand_category表,只需要在关联时将属性模板和商品类别的id写入中间表即可, 不需要对商品类别表进行修改,提升了数据库的写入效率.
- category与category_attribute_template关联, 帮助运营人员后台发布商品时快速调用属性模板.
- category与spu表关联, 用户在前台确定商品类别后, 以列表的形式展示相关商品数据.
- 商品类别表通过字段parent_id以自关联的方式来查找所属父级的信息从而构建类目树, 通过类目树, 可以在商品前台实现商品分类功能.
实施开发
@DubboService
@Service
@Slf4j
public class FrontCategoryServiceImpl implements IFrontCategoryService {
// 项目中涉及Redis的信息读取,定义这个常量,降低拼写错误风险
public static final String CATEGORY_TREE_KEY="category_tree";
// 利用Dubbo获得可以连接数据库获得所有分类信息的业务逻辑层方法
@DubboReference
private IForFrontCategoryService dubboCategoryService;
@Autowired
private RedisTemplate redisTemplate;
@Override
public FrontCategoryTreeVO categoryTree() {
// 凡是有Redis读取检查的都是这个模式
if(redisTemplate.hasKey(CATEGORY_TREE_KEY)){
// 如果Redis中包含分类树信息,直接从Redis中获取返回即可
FrontCategoryTreeVO<FrontCategoryEntity> treeVo=
(FrontCategoryTreeVO<FrontCategoryEntity>)
redisTemplate.boundValueOps(CATEGORY_TREE_KEY).get();
return treeVo;
}
// 如果Redis中没有数据,证明本次运行需要从数据库查询所有分类,拼接从三级分类树并返回
// 首先一定是先要从数据库中查询所有分类对象
List<CategoryStandardVO> categoryStandardVOs=
dubboCategoryService.getCategoryList();
// 将所有分类对象关联成三级分类树返回
FrontCategoryTreeVO<FrontCategoryEntity> treeVO=initTree(categoryStandardVOs);
// 将确定好的三级分类树保存到Redis
redisTemplate.boundValueOps(CATEGORY_TREE_KEY)
.set(treeVO, 24 , TimeUnit.HOURS);
// 千万别忘了返回!!!
return treeVO;
}
private FrontCategoryTreeVO<FrontCategoryEntity> initTree(List<CategoryStandardVO> categoryStandardVOs) {
// 第一部分,确定所有分类对象的父分类
// 声明一个Map,这个map的Key是父分类的Id,这个Map的Value是当前父分类的所有子分类对象
Map<Long,List<FrontCategoryEntity>> map=new HashMap<>();
// 日志输出分类对象个数
log.info("当前分类对象总数:{}",categoryStandardVOs.size());
// 下面编写for循环,遍历categoryStandardVOs集合,将其中的所有元素保存到父分类id值对应的Map中
// 根分类parentId是0
for(CategoryStandardVO categoryStandardVO : categoryStandardVOs){
// 因为CategoryStandardVO类型中没有children属性保存子分类对象
// 所以我们要使用FrontCategoryEntity来保存同名属性
FrontCategoryEntity frontCategoryEntity=new FrontCategoryEntity();
// 利用BeanUtils将categoryStandardVO同名属性赋值给frontCategoryEntity
BeanUtils.copyProperties(categoryStandardVO,frontCategoryEntity);
// 提取当前分类对象的父级id(parentId)
Long parentId=frontCategoryEntity.getParentId();
// 判断当前的父级Id是否在Map中已经存在
if(!map.containsKey(parentId)){
// 如果parentId是第一次出现,就要向map中添加一个元素
List<FrontCategoryEntity> value=new ArrayList<>();
value.add(frontCategoryEntity);
map.put(parentId,value);
}else{
// 如果当前以parentId值作为key的map元素已经存在,
// 我们就向当前map元素的List集合中添加当前分类对象即可
map.get(parentId).add(frontCategoryEntity);
}
}
log.info("当前map中包含父级id的个数为:{}",map.size());
// 第二部分,将每个分类对象关联到正确父分类对象中
// 我们已经获得了每个父分类包含了内些子分类的数据
// 下面就可以从根分类开始,通过循环遍历将每个分类对象包含的子分类添加到children属性中
// 因为根分类id为0,所以先key为0的获取
List<FrontCategoryEntity> firstLevels=map.get(0L);
//判断根分类是否为null
if(firstLevels==null){
throw new CoolSharkServiceException(ResponseCode.BAD_REQUEST,"当前项目没有根分类");
}
// 首先遍历我们从Map中获取的所有根分类
for(FrontCategoryEntity oneLevel: firstLevels){
// 获得当前根分类对象的id
Long secondLevelParentId=oneLevel.getId();
// map中获取当前分类对象的所有子分类的集合
List<FrontCategoryEntity> secondLevels=map.get(secondLevelParentId);
// 判断二级分类是否为null
if(secondLevels==null){
log.warn("当前分类缺少二级分类内容:{}",secondLevelParentId);
// 为了防止空集合遍历发生异常,我们直接跳过本次循环,运行下次循环
continue;
}
// 遍历当前根分类的所有二级分类
for(FrontCategoryEntity twoLevel : secondLevels){
// 获得当前二级分类对象id,作为三级分类的父id保存
Long thirdLevelParentId = twoLevel.getId();
// 获得当前二级分类的所有子元素
List<FrontCategoryEntity> thirdLevels=map.get(thirdLevelParentId);
if(thirdLevels==null){
log.warn("当前分类缺少三级分类内容:{}",thirdLevelParentId);
continue;
}
// 将三级分类对象集合赋值给二级分类对象的children属性
twoLevel.setChildrens(thirdLevels);
}
// 将二级分类对象集合赋值给一级分类对象的children属性
oneLevel.setChildrens(secondLevels);
}
// 将转换完成的所有一级分类对象,按方法要求返回
FrontCategoryTreeVO<FrontCategoryEntity> treeVO=new FrontCategoryTreeVO<>();
// 向对象中属性赋值
treeVO.setCategories(firstLevels);
// 别忘了修改返回treeVO
return treeVO;
}
}
按分类id分页查询Spu列表
按分类id分页查询Spu列表
用户会根据分类树中的分类的名称,查询它需要的商品类别
点击商品分类名称时,实际上我们获得了它的分类id(categoryId)
我们可以根据这个id到pms_spu表中查询商品信息
并进行分页显示
这个查询还是编写在front模块,但是查询spu的代码已经在product模块中完成了
下面就在业务逻辑层中创建FrontProductServiceImpl
@Service
public class FrontProductServiceImpl implements IFrontProductService {
@DubboReference
private IForFrontSpuService dubboSpuService;
@Override
public JsonPage<SpuListItemVO> listSpuByCategoryId(Long categoryId, Integer page, Integer pageSize) {
// IForFrontSpuService实现类中完成了分页步骤,所以我们直接调用即可
JsonPage<SpuListItemVO> spuListItemVOJsonPage=
dubboSpuService.listSpuByCategoryId(categoryId,page,pageSize);
// 千万别忘了返回spuListItemVOJsonPage
return spuListItemVOJsonPage;
}
@Override
public SpuStandardVO getFrontSpuById(Long id) {
return null;
}
@Override
public List<SkuStandardVO> getFrontSkusBySpuId(Long spuId) {
return null;
}
@Override
public SpuDetailStandardVO getSpuDetail(Long spuId) {
return null;
}
@Override
public List<AttributeStandardVO> getSpuAttributesBySpuId(Long spuId) {
return null;
}
}
转到控制层编写调用
创建FrontSpuController
代码如下
@RestController
@RequestMapping("/front/spu")
@Api(tags = "前台商品spu模块")
public class FrontSpuController {
@Autowired
private IFrontProductService frontProductService;
// localhost:10004/front/spu/list/3
@GetMapping("/list/{categoryId}")
@ApiOperation("根据分类id查询spu列表")
@ApiImplicitParams({
@ApiImplicitParam(value = "分类id",name = "categoryId",required = true,
dataType = "long"),
@ApiImplicitParam(value = "页码",name = "page",required = true,
dataType = "int"),
@ApiImplicitParam(value = "每页条数",name = "pageSize",required = true,
dataType = "int")
})
public JsonResult<JsonPage<SpuListItemVO>> listSpuByPage(
@PathVariable Long categoryId,Integer page,Integer pageSize){
JsonPage<SpuListItemVO> jsonPage=
frontProductService.listSpuByCategoryId(categoryId,page,pageSize);
return JsonResult.ok(jsonPage);
}
}
将前面章节启动的Order模块停止
然后再Nacos\Seata\Redis启动的前提下
顺序启动Leaf\Product\Front
进行测试
http://localhost:10004/doc.html
类别管理--后台
根据已经添加的数据表,目前此项目中需要管理的数据类型大致有:
- 品牌
- 类别
- 图片
- 相册
- 属性
- 属性模版
- SPU
- SKU
- 相关的关联表
首先,需要分析以上数据类型的开发先后顺序,部分数据之间存在依赖与被依赖关系,例如SKU肯定归属于某个SPU,在开发时,必须先开发SPU,再开发SKU,同理,必须先开发品牌,才可以开发SPU……
根据分析,必要的顺序为:(品牌 | 类别 | (相册 >>> 图片) | (属性模板 >>> 属性)) >>> SPU >>> SKU。
分析出必要顺序后,存在一些不需要严格区分顺序的数据类型,例如以上的品牌和类型,实际的开发顺序可以是先简单、后复杂,例如品牌数据通常比类别数据更加简单,则应该先开发品牌数据的管理,再开发类别数据的管理。
本次学习过程中,先开发类别,至于品牌,可自行课后完成!
当确定了需要处理类别的数据时,需要规划需要开发此数据的哪些管理功能,例如:添加类别、启用类别、禁用类别、修改类别的基本信息、根据id查询、根据parent_id查询列表……
以上管理类别数据的功能,开发顺序应该是:添加类别 >>> (根据id查询 | 根据parent_id查询列表) >>> (启用类别 | 禁用类别 | 修改类别的基本信息)
类别管理逻辑实现
关于以上业务的实现分析:
@Autowired
private CategoryMapper categoryMapper;
// 注意:需要创建异常
// 注意:需要在CategoryMapper中补充getById()方法,至少返回:depth
// 注意:需要在CategoryMapper中补充updateIsParentById()方法
public void addNew(CategoryAddNewDTO categoryAddNewDTO) {
// 从参数中取出尝试添加的类别的名称
// 调用categoryMapper.getByName()方法查询
// 判断查询结果是否不为null
// 是:抛出ServiceException
// 从参数中取出父级类别的id:parentId
// 判断parentId是否为0
// 是:此次尝试添加的是一级类别,没有父级类别,则当前depth >>> 1
// 否:此次尝试添加的不是一级类别,则应该存在父级类别,调用categoryMapper.getById()方法查询父级类别的信息
// -- 判断查询结果是否为null
// -- 是:抛出ServiceException
// -- 否:当前depth >>> 父级depth + 1
// 创建Category对象
// 调用BeanUtils.copyProperties()将参数对象中的属性值复制到Category对象中
// 补全Category对象中的属性值:depth >>> 前序运算结果
// 补全Category对象中的属性值:enable >>> 1(默认即启用)
// 补全Category对象中的属性值:isParent >>> 0
// 补全Category对象中的属性值:gmtCreate, gmtModified >>> LocalDateTime.now()
// 调用categoryMapper.insert(Category)插入类别数据,获取返回的受影响的行数
// 判断返回的受影响的行数是否不为1
// 是:抛出ServiceException
// 判断父级类别的isParent是否为0
// 是:调用categoryMapper.updateIsParentById()方法,将父级类别的isParent修改为1,获取返回的受影响的行数
// 判断返回的受影响的行数是否不为1
// 是:抛出ServiceException
}
商品详情
查询商品详情页
在商品列表中选中商品后,会显示这个商品的详情信息
我们需要显示的信息包括
- 根据spuId查询spu信息
- 根据spuId查询spuDetail详情
- 根据spuId查询当前Spu包含的所有属性
- 根据spuId查询对应的sku列表
继续编写FrontProductServiceImpl其他没有实现的方法
@Service
public class FrontProductServiceImpl implements IFrontProductService {
@DubboReference
private IForFrontSpuService dubboSpuService;
// 声明消费Sku相关的业务逻辑
@DubboReference
private IForFrontSkuService dubboSkuService;
// 声明消费商品参数选项(attribute)的业务逻辑
@DubboReference
private IForFrontAttributeService dubboAttributeService;
@Override
public JsonPage<SpuListItemVO> listSpuByCategoryId(Long categoryId, Integer page, Integer pageSize) {
// IForFrontSpuService实现类中完成了分页步骤,所以我们直接调用即可
JsonPage<SpuListItemVO> spuListItemVOJsonPage=
dubboSpuService.listSpuByCategoryId(categoryId,page,pageSize);
// 千万别忘了返回spuListItemVOJsonPage
return spuListItemVOJsonPage;
}
// 根据spuId查询Spu详情
@Override
public SpuStandardVO getFrontSpuById(Long id) {
// dubbo调用spu的方法即可
SpuStandardVO spuStandardVO=dubboSpuService.getSpuById(id);
return spuStandardVO;
}
// 根据spuId查询当前spu对应的所有sku商品列表
@Override
public List<SkuStandardVO> getFrontSkusBySpuId(Long spuId) {
List<SkuStandardVO> list=dubboSkuService.getSkusBySpuId(spuId);
return list;
}
// 根据spuId查询spuDetail详情
@Override
public SpuDetailStandardVO getSpuDetail(Long spuId) {
SpuDetailStandardVO spuDetailStandardVO=
dubboSpuService.getSpuDetailById(spuId);
return spuDetailStandardVO;
}
// 根据spuId查询当前商品的所有参数选项集合
@Override
public List<AttributeStandardVO> getSpuAttributesBySpuId(Long spuId) {
// dubbo调用Product模块编写好的方法
// 这个方法的sql语句是一个5表联查,需要额外注意
List<AttributeStandardVO> list=
dubboAttributeService.getSpuAttributesBySpuId(spuId);
return list;
}
}
根据spuid查询商品属性选项的sql语句
思路
1.根据spu_id去pms_spu表查询category_id
2.根据category_id去pms_category表查询分类对象
3.根据category_id去pms_category_attribute_template表查询attribute_template_id
4.根据attribute_template_id去pms_attribute_template表查询attribute_template数据行
5.根据attribute_template_id去pms_attribute表查询对应所有属性信息行
SELECT
pa.id , pa.template_id , pa.name , pa.description,
pa.`type` , pa.input_type , pa.value_list,
pa.unit , pa.sort , pa.is_allow_customize
FROM pms_spu ps
JOIN pms_category pc ON ps.category_id=pc.id
JOIN pms_category_attribute_template pcat
ON pc.id=pcat.category_id
JOIN pms_attribute_template pat
ON pat.id=pcat.attribute_template_id
JOIN pms_attribute pa ON pa.template_id=pat.id
WHERE ps.id=4
编写控制层代码
在FrontSpuController类中继续编写上面业务逻辑层方法的调用即可
@RestController
@RequestMapping("/front/spu")
@Api(tags = "前台商品spu模块")
public class FrontSpuController {
@Autowired
private IFrontProductService frontProductService;
// localhost:10004/front/spu/list/3
@GetMapping("/list/{categoryId}")
@ApiOperation("根据分类id查询spu列表")
@ApiImplicitParams({
@ApiImplicitParam(value = "分类id",name = "categoryId",required = true,
dataType = "long"),
@ApiImplicitParam(value = "页码",name = "page",required = true,
dataType = "int"),
@ApiImplicitParam(value = "每页条数",name = "pageSize",required = true,
dataType = "int")
})
public JsonResult<JsonPage<SpuListItemVO>> listSpuByPage(
@PathVariable Long categoryId,Integer page,Integer pageSize){
JsonPage<SpuListItemVO> jsonPage=
frontProductService.listSpuByCategoryId(categoryId,page,pageSize);
return JsonResult.ok(jsonPage);
}
// 根据spuId查询spu详情
// localhost:10004/front/spu/4
@GetMapping("/{id}")
@ApiOperation("根据spuId查询spu详情")
@ApiImplicitParams({
@ApiImplicitParam(value = "spuId",name = "id",required = true,
dataType = "long")
})
public JsonResult<SpuStandardVO> getFrontSpuById(@PathVariable Long id){
SpuStandardVO spuStandardVO=frontProductService.getFrontSpuById(id);
return JsonResult.ok(spuStandardVO);
}
// 根据spuId查询所有参数选项属性
@GetMapping("/template/{id}")
@ApiOperation("根据spuId查询所有参数选项属性")
@ApiImplicitParams({
@ApiImplicitParam(value = "spuId",name="id",required = true,
dataType = "long")
})
public JsonResult<List<AttributeStandardVO>> getAttributeBySpuId(
@PathVariable Long id){
List<AttributeStandardVO> list=frontProductService.getSpuAttributesBySpuId(id);
return JsonResult.ok(list);
}
}
新建sku控制器
FrontSkuController
@RestController
@RequestMapping("/front/sku")
@Api(tags = "商品前台sku模块")
public class FrontSkuController {
@Autowired
private IFrontProductService frontProductService;
// 根据spuId查询sku列表
@GetMapping("/{spuId}")
@ApiOperation("根据spuId查询sku列表")
@ApiImplicitParam(value = "spuId",name="spuId",required = true,
dataType = "long")
public JsonResult<List<SkuStandardVO>> getSkuListBySpuId(
@PathVariable Long spuId){
List<SkuStandardVO> list=frontProductService.getFrontSkusBySpuId(spuId);
return JsonResult.ok(list);
}
}
新建spu详情控制器
FrontSpuDetailController
@RestController
@RequestMapping("/front/spu/detail")
@Api(tags = "前台spuDetail模块")
public class FrontSpuDetailController {
@Autowired
private IFrontProductService frontProductService;
// 根据spuId查询spu的detail详情信息
@GetMapping("/{spuId}")
@ApiOperation("根据spuId查询spu的detail详情信息")
@ApiImplicitParam(value = "spuId", name = "spuId", required = true, dataType = "long")
public JsonResult<SpuDetailStandardVO> getSpuDetail(@PathVariable Long spuId){
SpuDetailStandardVO spuDetailStandardVO=frontProductService.getSpuDetail(spuId);
return JsonResult.ok(spuDetailStandardVO);
}
}