商城首页数据的接口实现

商城首页数据的接口在原单体 API 项目中已经实现,改造过程并不复杂,将相关的代码复制过来并进行一些修改即可。由于该接口主要涉及轮播图和首页推荐配置两个功能点,因此该接口是定义在推荐微服务中的。本节的源代码是在 newbee-mall-cloud-dev-step11 工程的基础上改造的,将工程命名为 newbee-mall-cloud-dev-step12

首页的排版设计

新蜂商城的商城首页布局和排版效果如图 6-12 所示。

image 2025 04 23 16 03 06 177
Figure 1. 图6-12 新蜂商城的商城首页布局和排版效果

商城首页的整个设计版面被分成 6 个部分,总结如下。

  1. 商城 Logo 及搜索框:可以放置 Logo 图片、商品搜索输入框。

  2. 轮播图:以轮播的形式展示后台配置的轮播图。

  3. 热销商品:展示后台配置的热销商品数据。

  4. 新品推荐:展示后台配置的新品数据。

  5. 推荐商品:展示后台配置的推荐商品数据。

  6. 导航栏:固定导航栏,放置新蜂商城几个重要的功能模块页面。

当然,以上版面设计只是针对新蜂商城这个项目,在开源社区或企业开发中还有许多其他的商城系统项目。前端的设计和实现灵活多变,不同的商城系统可能有不同的页面样式和页面布局。

首页接口的响应结果设计

因为首页展示的数据是多个功能模块的数据,包括轮播图数据、首页配置推荐数据、商品数据,所以需要重新抽象出一个首页数据视图层对象,并对数据格式进行规范和定义。

关于轮播图数据结构,需要将轮播图的图片地址和单击轮播图后的跳转路径返回给前端,因此定义了视图层对象 NewBeeMallIndexCarouselVO,字段定义如下:

/**
 * 首页轮播图 VO
 */
@Data
public class NewBeeMallIndexCarouselVO implements Serializable {

    @ApiModelProperty("轮播图图片地址")
    private String carouselUrl;

    @ApiModelProperty("点击轮播图后的跳转路径")
    private String redirectUrl;
}

在商品推荐管理模块中,通过首页效果图可以看到,每个推荐商品的展示区域都有 3 个字段,分别是商品名称、商品图片、商品价格。另外,因为单击后会跳转至商品详情页面,所以需要加上商品 id 字段,视图层对象 NewBeeMallIndexConfigGoodsVO 的字段定义如下:

/**
 * 首页配置商品VO
 */
@Data
public class NewBeeMallIndexConfigGoodsVO implements Serializable {

    @ApiModelProperty("商品id")
    private Long goodsId;
    @ApiModelProperty("商品名称")
    private String goodsName;
    @ApiModelProperty("商品图片地址")
    private String goodsCoverImg;
    @ApiModelProperty("商品价格")
    private Integer sellingPrice;
}

上述两个 VO 对象都是单项,只能表示一个轮播图对象和一个推荐商品对象。而在首页展示时,需要展示多条数据,即轮播图需要返回多张,推荐商品有 3 种类型,每种类型也有多条商品数据。因此需要返回 List 类型的数据,最终抽象出一个首页信息对象 IndexInfoVO,字段定义如下:

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.io.Serializable;
import java.util.List;

@Data
public class IndexInfoVO implements Serializable {
    @ApiModelProperty("轮播图 (列表)")
    private List<NewBeeMallIndexCarouselVO> carousels;

    @ApiModelProperty("首页热销商品 (列表)")
    private List<NewBeeMallIndexConfigGoodsVO> hotGoodses;

    @ApiModelProperty("首页新品推荐 (列表)")
    private List<NewBeeMallIndexConfigGoodsVO> newGoodses;

    @ApiModelProperty("首页推荐商品 (列表)")
    private List<NewBeeMallIndexConfigGoodsVO> recommendGoodses;
}

打开 newbee-mall-cloud-recommend-web 工程,新建 ltd.recommend.cloud.newbee.controller.vo 包,并创建上述的 3 个 VO 类。

VO 对象就是视图层使用的对象,与 entity 对象有一些区别。entity 对象中的字段与数据库表字段逐一对应,VO 对象里的字段是视图层需要哪些字段就设置哪些字段。在编码时,可以不去额外新增 VO 对象而直接返回 entity 对象,这取决于开发者的编码习惯。

这样,首页展示时后端需要返回的数据格式就定义完成了。如果读者对该项目进行了功能的删减,则可以参考笔者定义的视图层对象灵活地增减字段。

业务层代码的实现

因为获取首页的数据只涉及查询操作,所以业务层方法中分别定义了 getCarousels-ForIndex() 方法和 getConfigGoodsesForIndex() 方法用于查询首页接口中所需要的数据,查询数据后进行首页VO对象的封装。

getCarouselsForIndex() 方法的代码如下:

public List<NewBeeMallIndexCarouselVO> getCarouselsForIndex(int number) {
    List<NewBeeMallIndexCarouselVO> newBeeMallIndexCarouselVOS = new ArrayList<>(number);
    // 读取 MySQL 语句,查询固定数量的轮播图数据
    List<Carousel> carousels = carouselMapper.findCarouselsByNum(number);
    if (!CollectionUtils.isEmpty(carousels)) {
        // 将数据转换为需要的 VO 类型
        newBeeMallIndexCarouselVOS = BeanUtil.copyList(carousels,
                NewBeeMallIndexCarouselVO.class);
    }
    return newBeeMallIndexCarouselVOS;
}

getCarouselsForIndex() 方法的作用是返回固定数量的轮播图对象供首页数据渲染,执行逻辑如下:先查询固定数量的轮播图数据,参数为 number,再进行非空判断,然后将查询出来的轮播图对象转换为视图层对象 NewBeeMallIndexCarouselVO,最终返回给调用端。

getConfigGoodsesForIndex() 方法的代码如下:

public List<NewBeeMallIndexConfigGoodsVO> getConfigGoodsesForIndex(int configType, int number) {
    List<NewBeeMallIndexConfigGoodsVO> newBeeMallIndexConfigGoodsVOS = new ArrayList<>(number);
    List<IndexConfig> indexConfigs = indexConfigMapper.findIndexConfigsByTypeAndNum(configType, number);
    if (!CollectionUtils.isEmpty(indexConfigs)) {
        //取出所有的 goodsId
        List<Long> goodsIds = indexConfigs.stream().map(IndexConfig::getGoodsId).collect(Collectors.toList());
        //读取 MySQL 语句,根据商品 id 列表查询对应的商品数据
        List<NewBeeMallGoods> newBeeMallGoods = goodsMapper.selectByPrimaryKeys(goodsIds);
        newBeeMallIndexConfigGoodsVOS = BeanUtil.copyList(newBeeMallGoods, NewBeeMallIndexConfigGoodsVO.class);
        // 转换为 VO 对象
        for (NewBeeMallIndexConfigGoodsVO newBeeMallIndexConfigGoodsVO :
                newBeeMallIndexConfigGoodsVOS) {
            String goodsName = newBeeMallIndexConfigGoodsVO.getGoodsName();
            // 字符串过长导致文字超出的问题
            if (goodsName.length() > 30) {
                goodsName = goodsName.substring(0, 30) + "...";
                newBeeMallIndexConfigGoodsVO.setGoodsName(goodsName);
            }
        }
        return newBeeMallIndexConfigGoodsVOS;
    }
}

getConfigGoodsesForIndex() 方法的作用是返回一定数量的配置项对象供首页数据渲染,执行逻辑如下:先根据 configType 参数读取固定数量的首页配置数据,再获取配置项中关联的商品 id 列表,然后查询商品表并将首页展示时所需的几个字段依次读取并封装到 VO 对象里,最后返回给调用端。

打开 newbee-mall-cloud-recommend-web 工程,在 service.impl 包的 NewBeeMall-CarouselServiceImpl 类和 NewBeeMallIndexConfigServiceImpl 类中添加上述两个方法即可。

调用商品微服务进行数据的查询与封装

按照上述步骤对推荐微服务进行编码后,代码依然会被标红,getConfigGoodsesForIndex() 方法中需要根据商品 id 列表查询对应的商品数据,才能进行后续的 VO 数据封装。而推荐微服务未连接商品表所在的数据库,无法直接通过 GoodsMapper 去查询对应的商品数据,因此这部分代码会被标红。

所以,这里必须进行商品数据查询逻辑的改造,由原本直接查询商品表改为远程调用商品微服务中的接口来完成这个逻辑。在之前的改造步骤中,推荐微服务已经引入了 goods-api 依赖并增加了关于商品微服务中 FeignClient 的配置,因此这部分配置可以省略,直接在商品微服务中编写一个根据 goodsId 查询商品列表数据的接口并暴露即可,改造步骤如下。

第一步,增加根据 goodsId 查询商品列表数据的接口。

打开 newbee-mall-cloud-goods-web 工程,在 NewBeeAdminGoodsInfoController 类中新增接口代码:

/**
 * 根据 goodsId 查询商品列表
 */
@GetMapping("/listByGoodsIds")
@ApiOperation(value = "根据 goodsId 查询商品列表", notes = "根据 goodsId 查询")
public Result getNewBeeMallGoodsByIds(@RequestParam("goodsIds") List<Long> goodsIds) {
    List<NewBeeMallGoods> newBeeMallGoods =
            newBeeMallGoodsService.getNewBeeMallGoodsByIds(goodsIds);
    return ResultGenerator.genSuccessResult(newBeeMallGoods);
}

打开 newbee-mall-cloud-goods-api 工程,在 NewBeeCloudGoodsServiceFeign 类中定义该接口,使得其他微服务可以调用,新增代码如下:

@GetMapping(value = "/admin/listByGoodsIds")
Result<List<NewBeeMallGoodsDTO>> listByGoodsIds(@RequestParam(value = "goodsIds") List<Long> goodsIds);

第二步,修改推荐微服务中的商品查询代码。

修改 NewBeeMallIndexConfigServiceImpl 类中商品列表数据查询的代码。删除原本直接查询商品数据库的代码,通过调用商品微服务来获取商品的数据,修改后的 getConfigGoodsesForIndex() 方法代码如下:

public List<NewBeeMallIndexConfigGoodsVO> getConfigGoodsesForIndex(int configType, int number) {
    List<NewBeeMallIndexConfigGoodsVO> newBeeMallIndexConfigGoodsVOS = new ArrayList<>(number);
    List<IndexConfig> indexConfigs =
            indexConfigMapper.findIndexConfigsByTypeAndNum(configType, number);
    if (!CollectionUtils.isEmpty(indexConfigs)) {
        // 取出所有的 goodsId
        List<Long> goodsIds =
                indexConfigs.stream().map(IndexConfig::getGoodsId).collect(Collectors.toList());
        // 调用商品微服务来获取商品的数据
        Result<List<NewBeeMallGoodsDTO>> newBeeMallGoodsDTOResult =
                goodsService.listByGoodsIds(goodsIds);
        if (newBeeMallGoodsDTOResult == null ||
                newBeeMallGoodsDTOResult.getResultCode() != 200 ||
                CollectionUtils.isEmpty(newBeeMallGoodsDTOResult.getData())) {
            // 未查询到数据,返回空链表(也可以直接在这里抛出异常)
            return newBeeMallIndexConfigGoodsVOS;
        }
        newBeeMallIndexConfigGoodsVOS =
                BeanUtil.copyList(newBeeMallGoodsDTOResult.getData(),
                        NewBeeMallIndexConfigGoodsVO.class);
        for (NewBeeMallIndexConfigGoodsVO newBeeMallIndexConfigGoodsVO :
                newBeeMallIndexConfigGoodsVOS) {
            String goodsName = newBeeMallIndexConfigGoodsVO.getGoodsName();
            String goodsIntro = newBeeMallIndexConfigGoodsVO.getGoodsIntro();
            // 字符串过长导致文字超出的问题
            if (goodsName.length() > 30) {
                goodsName = goodsName.substring(0, 30) + "...";
                newBeeMallIndexConfigGoodsVO.setGoodsName(goodsName);
            }
            if (goodsIntro.length() > 22) {
                goodsIntro = goodsIntro.substring(0, 22) + "...";
                newBeeMallIndexConfigGoodsVO.setGoodsIntro(goodsIntro);
            }
        }
        return newBeeMallIndexConfigGoodsVOS;
    }
}

这里调用的就是在商品微服务中暴露的 /goods/admin/listByGoodsIds 接口,根据 goodsId 列表查询对应的商品数据。

首页接口控制层代码的实现

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

package ltd.recommend.cloud.newbee.controller;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import ltd.common.cloud.newbee.dto.Result;
import ltd.common.cloud.newbee.dto.ResultGenerator;
import ltd.common.cloud.newbee.enums.IndexConfigTypeEnum;

import ltd.recommend.cloud.newbee.controller.vo.IndexInfoVO;
import ltd.recommend.cloud.newbee.controller.vo.NewBeeMallIndexCarouselVO;
import ltd.recommend.cloud.newbee.controller.vo.NewBeeMallIndexConfigGoodsVO;
import ltd.recommend.cloud.newbee.service.NewBeeMallCarouselService;
import ltd.recommend.cloud.newbee.service.NewBeeMallIndexConfigService;
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("/mall/index")
public class NewBeeMallIndexController {

    @Resource
    private NewBeeMallCarouselService newBeeMallCarouselService;

    @Resource
    private NewBeeMallIndexConfigService newBeeMallIndexConfigService;

    @GetMapping("/recommendInfos")
    @ApiOperation(value = "获取首页数据", notes = "轮播图、新品、推荐等")
    public Result<IndexInfoVO> indexInfo() {
        IndexInfoVO indexInfoVO = new IndexInfoVO();
        List<NewBeeMallIndexCarouselVO> carousels = newBeeMallCarouselService.getCarouselsForIndex(5);
        List<NewBeeMallIndexConfigGoodsVO> hotGoods = newBeeMallIndexConfigService.getConfigGoodsForIndex(IndexConfigTypeEnum.INDEX_GOODS_HOT.getType(), 4);
        List<NewBeeMallIndexConfigGoodsVO> newGoods = newBeeMallIndexConfigService.getConfigGoodsForIndex(IndexConfigTypeEnum.INDEX_GOODS_NEW.getType(), 6);
        List<NewBeeMallIndexConfigGoodsVO> recommendGoods = newBeeMallIndexConfigService.getConfigGoodsForIndex(IndexConfigTypeEnum.INDEX_GOODS_RECOMMEND.getType(), 10);
        indexInfoVO.setCarousels(carousels);
        indexInfoVO.setHotGoods(hotGoods);
        indexInfoVO.setNewGoods(newGoods);
        indexInfoVO.setRecommendGoods(recommendGoods);
        return ResultGenerator.genSuccessResult(indexInfoVO);
    }
}

处理首页数据请求的方法名称为 indexInfo(),请求类型为 GET,映射的路径为 /mall/index/recommondInfos,响应结果类型为统一的响应对象 Result,实际的 data 属性类型为 IndexInfoVO 视图层对象。

实现逻辑是分别调用轮播图业务实现类 NewBeeMallCarouselService 中的查询方法和首页配置业务实现类 NewBeeMallIndexConfigService 中的查询方法,查询首页所需的数据并逐一设置到 IndexInfoVO 对象中。最终响应给前端,因为商品推荐有热销商品、新品上线、推荐商品 3 种类型,所以首页配置业务实现类中的 getConfigGoodsesForIndex() 方法在此处被调用了 3 次,但是每次传入的参数并不相同。

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

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

// 登录接口、注册接口、首页接口,直接放行
if ("/users/mall/login".equals(uri) || "/users/mall/register".equals(uri)
    || "/mall/index/recommendInfos".equals(uri)) {
    return chain.filter(exchange);
}

首页接口网关配置

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

# 首页接口的路由配置
spring.cloud.gateway.routes[1].id=recommend-service-route
spring.cloud.gateway.routes[1].uri=lb://newbee-mall-cloud-recommend-service
spring.cloud.gateway.routes[1].order=1
spring.cloud.gateway.routes[1].predicates[0]=Path=/mall/index/**

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