商城分类页面的接口实现

本节的源代码是在 newbee-mall-cloud-dev-step12 工程的基础上改造的,将工程命名为 newbee-mall-cloud-dev-step13

分类页面的接口响应数据

分类页面的页面布局和交互依然由前端代码来实现,后端只要将页面所需的分类数据通过接口响应回去即可。接下来定义分类接口中返回数据的格式,这里结合新蜂商城的商城分类页面的布局和排版来讲解,如图 6-13 所示。

image 2025 04 28 10 57 48 159
Figure 1. 图6-13 新蜂商城的商城分类页面的布局和排版

由图 6-13 可知,分类页面中需要返回分类的列表数据。同时,分类信息有层级关系,分别是一级分类、二级分类和三级分类,并且三者的展示位置不同。一级分类固定在页面的左侧,由上至下平铺显示。二级分类和三级分类展示在页面右侧,每个二级分类下展示对应三级分类的列表,三级分类由左至右平铺展示,二级分类则分开展示。同时,二级分类数据和三级分类数据的展示区域会随着一级分类的切换动态变化。

在分类接口的返回数据格式定义中,需要返回一级分类列表,还包括每个一级分类的二级分类列表,而二级分类列表中的每个二级分类也有一个三级分类列表。因此,后端 API 项目中定义了 3 个视图层的分类 VO 对象,并做了层级的定义和关联。

因为都是分类信息,所以 3 个 VO 对象中都有分类级别和分类名称,三级分类的视图层对象 ThirdLevelCategoryVO 字段定义如下:

/**
 * 分类数据VO(第三级)
 */
@Data
public class ThirdLevelCategoryVO implements Serializable {

    @ApiModelProperty("当前三级分类id")
    private Long categoryId;

    @ApiModelProperty("当前分类级别")
    private Byte categoryLevel;

    @ApiModelProperty("当前三级分类名称")
    private String categoryName;
}

一级分类的视图层对象 NewBeeMallIndexCategoryVO 和二级分类的视图层对象 SecondLevelCategoryVO 字段定义如下:

/**
 * 分类数据VO(第二级)
 */
@Data
public class SecondLevelCategoryVO implements Serializable {

    @ApiModelProperty("当前二级分类id")
    private Long categoryId;

    @ApiModelProperty("父级分类id")
    private Long parentId;

    @ApiModelProperty("当前分类级别")
    private Byte categoryLevel;
    @ApiModelProperty("当前二级分类名称")
    private String categoryName;

    @ApiModelProperty("三级分类列表")
    private List<ThirdLevelCategoryVO> thirdLevelCategoryVOS;
}
/**
 * 分类数据VO
 */
@Data
public class NewBeeMallIndexCategoryVO implements Serializable {

    @ApiModelProperty("当前一级分类id")
    private Long categoryId;

    @ApiModelProperty("当前一级分类级别")
    private Byte categoryLevel;

    @ApiModelProperty("当前一级分类名称")
    private String categoryName;

    @ApiModelProperty("二级分类列表")
    private List<SecondLevelCategoryVO> secondLevelCategoryVOS;
}

在分类的 VO 对象中都定义了分类层级字段,并且在一级分类 VO 对象和二级分类 VO 对象中定义了下级分类的列表字段。比如,在二级分类中,不仅包含二级分类的信息,还包含该二级分类下所有的三级分类的信息。

打开 newbee-mall-cloud-goods-web 工程,新建 ltd.goods.cloud.newbee.controller.vo 包,创建上述 3 个 VO 对象即可。

业务层代码的实现

因为获取分类页面的数据只涉及查询操作,所以业务层方法中定义了 getCategories-ForIndex() 方法用于查询分类页面中所需要的数据,查询到数据后进行首页 VO 对象的封装。代码及注释信息如下:

import java.util.ArrayList;
public List<NewBeeMallIndexCategoryVO> getCategoriesForIndex() {
    List<NewBeeMallIndexCategoryVO> newBeeMallIndexCategoryVOS = new ArrayList<>();
    // 获取一级分类的固定数量的数据
    ArrayList<List<GoodsCategory>> firstLevelCategories =
            goodsCategoryMapper.selectByLevelAndParentIdsAndNumber(Collections.singletonList(0L), NewBeeMallCategoryLevelEnum.LEVEL_ONE.getLevel(),
                    Constants.INDEX_CATEGORY_NUMBER);
    if (!CollectionUtils.isEmpty(firstLevelCategories)) {
        List<Long> firstLevelCategoryIds =
                firstLevelCategories.stream().map(GoodsCategory::getCategoryId).collect(Collectors.toList());
        //获取二级分类的数据
        List<GoodsCategory> secondLevelCategories =
                goodsCategoryMapper.selectByLevelAndParentIdsAndNumber(firstLevelCategoryIds, NewBeeMallCategoryLevelEnum.LEVEL_TWO.getLevel(), 0);
        if (!CollectionUtils.isEmpty(secondLevelCategories)) {
            List<Long> secondLevelCategoryIds =
                    secondLevelCategories.stream().map(GoodsCategory::getCategoryId).collect(Collectors.toList());
            //获取三级分类的数据
            List<GoodsCategory> thirdLevelCategories =
                    goodsCategoryMapper.selectByLevelAndParentIdsAndNumber(secondLevelCategoryIds, NewBeeMallCategoryLevelEnum.LEVEL_THREE.getLevel(), 0);
            if (!CollectionUtils.isEmpty(thirdLevelCategories)) {
                //根据 parentId 将 thirdLevelCategories 分组
                Map<Long, List<GoodsCategory>> thirdLevelCategoryMap =
                        thirdLevelCategories.stream().collect(groupingBy(GoodsCategory::getParentId));
                List<SecondLevelCategoryVO> secondLevelCategoryVOS = new ArrayList<>();
                //处理二级分类
                for (GoodsCategory secondLevelCategory : secondLevelCategories) {
                    SecondLevelCategoryVO secondLevelCategoryVO = new SecondLevelCategoryVO();
                    BeanUtil.copyProperties(secondLevelCategory, secondLevelCategoryVO);
                    //如果该二级分类下有三级分类,则放入 secondLevelCategoryVOS 对象
                    if (thirdLevelCategoryMap.containsKey(secondLevelCategory.getCategoryId())) {
                        //根据二级分类的 id 取出 thirdLevelCategoryMap 分组中的三级分类列表
                        List<GoodsCategory> tempGoodsCategories = thirdLevelCategoryMap.get(secondLevelCategory.getCategoryId());
                        secondLevelCategoryVO.setThirdLevelCategoryVOS((BeanUtil.copyList(tempGoodsCategories, ThirdLevelCategoryVO.class)));
                        secondLevelCategoryVOS.add(secondLevelCategoryVO);
                    }
                }
                //处理一级分类
                if (!CollectionUtils.isEmpty(secondLevelCategoryVOS)) {
                    //根据 parentId 将 thirdLevelCategories 分组
                    Map<Long, List<SecondLevelCategoryVO>> secondLevelCategoryVOMap =
                            secondLevelCategoryVOS.stream().collect(groupingBy(SecondLevelCategoryVO::getParentId));

                    for (GoodsCategory firstCategory : firstLevelCategories) {
                        NewBeeMallIndexCategoryVO newBeeMallIndexCategoryVO = new NewBeeMallIndexCategoryVO();
                        BeanUtil.copyProperties(firstCategory, newBeeMallIndexCategoryVO);
                        //如果该一级分类下有数据,则放入 newBeeMallIndexCategoryVOS 对象
                        if (secondLevelCategoryVOMap.containsKey(firstCategory.getCategoryId())) {
                            //根据一级分类的 id 取出 secondLevelCategoryVOMap 分组中的二级分类列表
                            List<SecondLevelCategoryVO> tempGoodsCategories =
                                    secondLevelCategoryVOMap.get(firstCategory.getCategoryId());
                            newBeeMallIndexCategoryVO.setSecondLevelCategoryVOS(
                                    tempGoodsCategories
                            );
                            newBeeMallIndexCategoryVOS.add(newBeeMallIndexCategoryVO);
                        }
                    }
                    return newBeeMallIndexCategoryVOS;
                } else {
                    return null;
                }
            }
        }
    }
}

getCategoriesForIndex() 方法的作用是返回已配置完成的分类数据并响应给前端,实现思路如下:先读取固定数量的一级分类数据,再获取二级分类数据并设置到对应的一级分类下,然后获取和设置每个二级分类下的三级分类数据,接着将所有的分类列表数据都读取出来并根据层级进行划分和封装,最后将视图层对象返回给调用端。

下面结合代码具体讲解。查询一级分类列表的代码如下:

// 获取一级分类的固定数量的数据
List<GoodsCategory> firstLevelCategories =
        goodsCategoryMapper.selectByLevelAndParentIdsAndNumber(Collections.singletonList(0L), NewBeeMallCategoryLevelEnum.LEVEL_ONE.getLevel(),
                Constants.INDEX_CATEGORY_NUMBER);

因为一级分类是没有父类的,即父级分类的 id 为默认值 0,同时,parentIds 参数为 List 类型,所以这里 parentIds 参数传的是 Collections.singletonList(0L),分类级别传的是 1,用的是枚举类 NewBeeMallCategoryLevelEnum.LEVEL_ONE。查询数量是 10 条,用的也是一个常量 Constants.INDEX_CATEGORY_NUMBER,该值默认为 10。当然,这里直接传数字 10 也是可以的。

查询二级分类列表的代码如下:

// 获取二级分类的数据
List<GoodsCategory> secondLevelCategories =
goodsCategoryMapper.selectByLevelAndParentIdsAndNumber(firstLevelCategoryIds, NewBeeMallCategoryLevelEnum.LEVEL_TWO.getLevel(), 0);

因为上一步已经获取了所有的一级分类列表数据,所以把其中的 id 字段全部取出来并放到一个 List 对象 firstLevelCategoryIds 中,之后作为查询二级分类列表的 parentIds 参数。分类级别传的是 2,用的是枚举类 NewBeeMallCategoryLevelEnum.LEVEL_TWO。数量 number 参数传的是 0,表示查询所有当前一级分类下的二级分类数据,并不是代表查询 0 条数据。三级分类查询方式与二级分类查询方式类似,这里不再赘述。

打开 newbee-mall-cloud-goods-web 工程,在 service.impl 包的 NewBeeMallCategoryServiceImpl 类中添加上述方法即可。

分类页面数据接口控制层代码的实现

增加分类页面接口的定义与编码。打开 newbee-mall-cloud-goods-web 工程,选择 ltd.goods.cloud.newbee.controller 包并单击鼠标右键,在弹出的快捷菜单中选择 “New→Java Class” 选项,在弹出的窗口中输入 “NewBeeMallGoodsCategoryController”,对分类页面的数据请求进行处理,在 NewBeeMallGoodsCategoryController 类中新增如下代码:

package ltd.goods.cloud.newbee.controller;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import ltd.common.cloud.newbee.ServiceResultEnum;
import ltd.common.cloud.newbee.dto.Result;
import ltd.common.cloud.newbee.dto.ResultGenerator;
import ltd.common.cloud.newbee.exception.NewBeeMallException;
import ltd.goods.cloud.newbee.controller.vo.NewBeeMallIndexCategoryVO;
import ltd.goods.cloud.newbee.service.NewBeeMallCategoryService;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;

@RestController
@Api(value = "v1", tags = "新蜂商城分类页面接口")
@RequestMapping("/categories/mall")
public class NewBeeMallGoodsCategoryController {

    @Resource
    private NewBeeMallCategoryService newBeeMallCategoryService;

    @GetMapping("/listAll")
    @ApiOperation(value = "获取分类数据", notes = "分类页面使用")
    public Result<List<NewBeeMallIndexCategoryVO>> getCategories() {
        List<NewBeeMallIndexCategoryVO> categories = newBeeMallCategoryService.getCategoriesForIndex();
        if (CollectionUtils.isEmpty(categories)) {
            NewBeeMallException.fail(ServiceResultEnum.DATA_NOT_EXIST.getResult());
        }
        return ResultGenerator.genSuccessResult(categories);
    }
}

处理分类列表数据请求的方法名称为 getCategories(),请求类型为 GET,映射的路径为 /categories/mall/listAll,响应结果类型为 Result,实际的 data 属性类型为 List 对象,即分类列表数据。

实现逻辑是调用分类业务实现类 NewBeeMallCategoryService 中的查询方法,查询所需的数据并响应给前端,所有的实现逻辑都是在业务实现类中处理的,包括查询和字段设置,在控制层代码中只是将获得的数据结果设置给 Result 对象并返回。

还有一点需要注意,分类页面的内容是任何访问者都能够直接查看的,并不需要登录认证,因此首页数据接口中并没有使用权限认证的注解 @TokenToMallUser

由于无须登录认证,因此需要在商城端网关的全局过滤器中添加配置,在鉴权时忽略这个接口,直接放行。代码修改如下:

final List<String> ignoreURLs = new ArrayList<>();
ignoreURLs.add("/users/mall/login");
ignoreURLs.add("/users/mall/register");
ignoreURLs.add("/categories/mall/listAll");

ignoreURLs.add("/mall/index");

// 登录接口、注册接口、首页接口、分类接口,直接放行
if (ignoreURLs.contains(exchange.getRequest().getURI().getPath())) {
    return chain.filter(exchange);
}

在当前版本中,分类数据的获取是一个单独的接口。换一种思路来设计该接口,可以做成三个接口。第一个接口返回所有的一级分类数据,第二个接口根据选择的一级分类id查询所有的二级分类数据,第三个接口则根据选择的二级分类id查询所有的三级分类数据。笔者认为,一次性全部查出来的设计更好一些,这种接口设计方式可以让前端开发人员一次性处理和渲染这些数据,而不是分多次查询、渲染页面。

做成单独一个接口,前端方便处理,既不用因为用户切换了不同的分类而重新改变页面 DOM,也不用根据页面选项卡的切换多次发送请求,在一定程度上可以节省网络开销。不过,在做接口设计时,依然要因地制宜、灵活变通,并不是说笔者的想法就是正确的,读者要结合自己实际开发的项目灵活地分析和设计。

分类接口网关配置

打开 newbee-mall-cloud-gateway-mall 项目中的 application.properties 文件,新增关于商品微服务的路由信息,配置项为 spring.cloud.gateway.routes.*,新增内容如下:

# 分类接口的路由配置
spring.cloud.gateway.routes[2].id=goods-service-route
spring.cloud.gateway.routes[2].uri=lb://newbee-mall-cloud-goods-service
spring.cloud.gateway.routes[2].order=1
spring.cloud.gateway.routes[2].predicates[0]=Path=/categories/mall/**

以上是配置 newbee-mall-cloud-gateway-mall 到商品微服务的路由信息,主要配置 “获取分类页面数据” 的接口。如果访问商城端网关项目的路径是以 /categories/mall/ 开头的,就路由到商品微服务实例。