商品列表和商品详情页面的接口实现
首页接口和分类接口已经介绍完毕,接下来进行商品列表和商品详情页面的功能完善,需要实现商品搜索和商品详情页面的后端接口,本节的源代码是在 newbee-mall-cloud-dev-step13
工程的基础上改造的,将工程命名为 newbee-mall-cloud-dev-step13-2
。
接口传参解析及返回字段定义
获取用户输入的搜索关键字或分类 id
,跳转到搜索结果页面,以及搜索结果页面渲染,这些都是前端开发人员来实现的。前端页面在实现这些效果时,需要获取当前页面要渲染的数据,也就是在跳转到搜索结果页面后,组装商品搜索的请求参数并向后端接口发送搜索请求,获得后端返回的数据后进行页面渲染。后端开发人员需要定义接口参数、接口地址、接口返回字段,后端接口需要接收前端传过来的参数,先根据参数查询数据,再组装数据并返回给前端,最后由前端进行渲染。
-
接口传参
页面 UI 还原和样式编写是由前端开发人员来实现的,前端开发人员包括 Web 开发人员和移动端原生
App
开发人员,负责页面及交互。后端开发人员则需要实现一个列表数据的返回接口,根据用户输入的关键字或用户单击的分类来返回数据,即商品搜索接口。因此,搜索接口传参的两个重要参数为搜索关键字和商品的三级分类id
,将其分别定义为keyword
和goodsCategoryId
,类型分别为String
和Long
。另外,还有两个参数是排序方式orderBy
和分页参数pageNumber
。商品详情页面是让用户看到更多的商品信息,以便更好地进行选择和比较。不过,获取商品详情信息这个接口并不是一个复杂的逻辑,实现逻辑就是根据商品
id
查询商品表中的记录并返回给前端。 -
分页逻辑
虽然只是返回一个列表,但是有一个隐藏的知识点,那就是该接口返回的数据条数可能很多,需要加入分页的逻辑。在展示搜索内容后,商品列表页面可以不断往上拉,数据会不断地加载进来,在移动端的实现效果就是人们常说的 “上拉加载”,这里就用到了分页逻辑。虽然没有分页页码和翻页按钮,但依然是分页展示的逻辑,移动端页面通常的做法就是如此,毕竟人们在移动端的操作习惯与在
PC
端的操作习惯不同,移动端页面的面积也比PC
端页面的面积小了很多,不可能完全做成PC
端的分页效果。分页数据还做了一个封装,由于所有的分页结果基本上都包括以下几个属性,因此分页结果集的数据格式定义如下(注:完整代码位于
ltd.common.cloud.newbee.dto.PageResult
)://分页的通用结果类 public class PageResult implements Serializable { //总记录数 private int totalCount; //每页记录数 private int pageSize; //总页码 private int totalPage; //当前页的页码 private int currPage; //当前页的所有数据列表 private List<?> list; }
在实现分页功能的返回对象
PageResult
中定义了以下 4 个参数,即当前页的所有数据列表、当前页的页码、总页码、总记录数,并将它们放入Result
返回结果的data
属性中,在商品搜索接口中我们最终得到的返回对象为Result<PageResult<Lis<NewBeeMallSearchGoodsVO>>>
,最外层是Result
对象,这个不用多说,所有的接口返回类型都是Result
类型,里面一层是PageResult
对象,因为搜索接口需要返回分页信息,PageResult
对象中则是具体的当前页所需要的商品列表信息List<NewBeeMallSearchGoodsVO>
。之后由前端开发人员直接读取对应的参数并对这些数据进行处理,这就是前后端进行数据交互时分页数据的格式定义,希望读者能够结合代码及实际的分页效果进行理解和学习。
-
返回数据格式
图 6-14 所示为商品列表页面和商品详情页面中需要渲染的内容。商品分页列表是一个 List
对象,还有分页功能,需要返回分页字段,因此最终接收的结果返回格式为 PageResult
对象,而列表的单项对象中的字段则需要通过图 6-14 中的内容进行确认。

通过图片可以看到商品预览图字段、商品标题字段、商品简介字段、商品价格字段。当然,这里通常会设计成可跳转的形式,即单击标题或预览图会跳转到对应的商品详情页面中,因此还需要一个商品实体的 id
字段。这样,返回数据的格式就得出来了,代码如下:
package ltd.goods.cloud.newbee.controller.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 搜索列表页面商品 VO
*/
@Data
public class NewBeeMallSearchGoodsVO implements Serializable {
@ApiModelProperty("商品 id")
private Long goodsId;
@ApiModelProperty("商品名称")
private String goodsName;
@ApiModelProperty("商品简介")
private String goodsIntro;
@ApiModelProperty("商品图片地址")
private String goodsCoverImg;
@ApiModelProperty("商品价格")
private Integer sellingPrice;
}
商品详情页面需要展示的字段更多一些,数据格式如下:
/**
* 商品详情页面 VO
*/
@Data
public class NewBeeMallGoodsDetailVO implements Serializable {
@ApiModelProperty("商品 id")
private Long goodsId;
@ApiModelProperty("商品名称")
private String goodsName;
@ApiModelProperty("商品简介")
private String goodsIntro;
@ApiModelProperty("商品图片地址")
private String goodsCoverImg;
@ApiModelProperty("商品价格")
private Integer sellingPrice;
@ApiModelProperty("商品标签")
private String tag;
@ApiModelProperty("商品图片")
private String[] goodsCarouselList;
@ApiModelProperty("商品原价")
private Integer originalPrice;
@ApiModelProperty("商品详情内容")
private String goodsDetailContent;
}
打开 newbee-mall-cloud-goods-web
工程,在 ltd.goods.cloud.newbee.controller.vo
包中增加上述两个 VO
类即可。
业务层代码的实现
获取商品列表的数据只涉及查询操作,因此业务层方法中定义了 searchNewBeeMallGoods()
方法用于查询分类页面中所需要的数据,查询到数据后进行分页对象的封装。查询商品详情数据则更加简单,根据主键查询对应的数据库记录即可,代码如下:
public PageResult searchNewBeeMallGoods(PageQueryUtil pageUtil) {
List<NewBeeMallGoods> goodsList = goodsMapper.findNewBeeMallGoodsListBySearch(pageUtil);
int total = goodsMapper.getTotalNewBeeMallGoodsBySearch(pageUtil);
List<NewBeeMallSearchGoodsVO> newBeeMallSearchGoodsVOS = new ArrayList<NewBeeMallSearchGoodsVO>();
if (!CollectionUtils.isEmpty(goodsList)) {
List<NewBeeMallSearchGoodsVO> newBeeMallSearchGoodsVOS = BeanUtil.copyList(goodsList, NewBeeMallSearchGoodsVO.class);
for (NewBeeMallSearchGoodsVO newBeeMallSearchGoodsVO : newBeeMallSearchGoodsVOS) {
String goodsName = newBeeMallSearchGoodsVO.getGoodsName();
String goodsIntro = newBeeMallSearchGoodsVO.getGoodsIntro();
// 字符串过长导致文字超出的问题
if (goodsName.length() > 28) {
goodsName = goodsName.substring(0, 28) + "...";
newBeeMallSearchGoodsVO.setGoodsName(goodsName);
}
if (goodsIntro.length() > 30) {
goodsIntro = goodsIntro.substring(0, 30) + "...";
newBeeMallSearchGoodsVO.setGoodsIntro(goodsIntro);
}
}
PageResult pageResult = new PageResult(newBeeMallSearchGoodsVOS, total, pageUtil.getLimit(), pageUtil.getPage());
return pageResult;
}
public NewBeeMallGoods getNewBeeMallGoodsById (Long id){
NewBeeMallGoods newBeeMallGoods = goodsMapper.selectByPrimaryKey(id);
if (newBeeMallGoods == null) {
NewBeeMallException.fail(ServiceResultEnum.GOODS_NOT_EXIST.getResult());
}
return newBeeMallGoods;
}
}
searchNewBeeMallGoods()
方法使用 PageQueryUtil
对象作为参数,商品类目 id
、关键字 keyword
字段、分页所需的 page
字段、排序字段等都作为属性放在了这个对象中,关键字或商品类目 id
用来过滤想要的商品列表,page
字段用于确定查询第几页的数据,之后通过 SQL
语句查询对应的分页数据,并填充数据。某些字段太长导致页面上的展示效果不好,所以对这些字段进行了简单的字符串处理并设置到 NewBeeMallSearchGoodsVO
对象中,最终返回的数据是 PageResult
对象,包含当前页返回的商品列表数据和分页信息。
打开 newbee-mall-cloud-goods-web
工程,在 service.impl
包的 NewBeeMallCategoryServiceImpl
类中添加上述方法即可。
具体执行的 SQL
语句如下(注:完整代码位于 resources/mapper/NewBeeMallGoodsMapper.xml
文件中):
<select id="findNewBeeMallGoodsListBySearch" parameterType="Map" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from tb_newbee_mall_goods_info
<where>
<if test="keyword!=null and keyword!=''">
and (goods_name like CONCAT('%',#{keyword},'%') or goods_intro like CONCAT('%',#{keyword},'%'))
</if>
<if test="goodsCategoryId!=null and goodsCategoryId!=''">
and goods_category_id = #{goodsCategoryId}
</if>
</where>
<if test="orderBy!=null and orderBy!=''">
<choose>
<when test="orderBy == 'new'">
order by goods_id desc
</when>
<when test="orderBy == 'price'">
order by selling_price asc
</when>
<otherwise>
order by stock_num desc
</otherwise>
</choose>
</if>
<if test="start!=null and limit!=null">
limit #{start},#{limit}
</if>
</select>
<select id="getTotalNewBeeMallGoodsBySearch" parameterType="Map" resultType="int">
select count(*) from tb_newbee_mall_goods_info
<where>
<if test="keyword!=null and keyword!=''">
and (goods_name like CONCAT('%',#{keyword},'%') or goods_intro like CONCAT('%',#{keyword},'%'))
</if>
<if test="goodsCategoryId!=null and goodsCategoryId!=''">
and goods_category_id = #{goodsCategoryId}
</if>
</where>
</select>
根据前端传过来的关键字和商品类目 id
对商品记录进行检索,使用 MySQL
数据库的 LIKE
语法对关键字进行过滤,或者根据 goods_category_id
字段对商品类目进行过滤,之后根据 orderBy
字段进行商品搜索分页结果的排序,最后使用 start
和 limit
两个分页所必需的关键字。
控制层代码的实现
增加商品列表接口的定义与编码。打开 newbee-mall-cloud-goods-web
工程,选择 ltd.goods.cloud.newbee.controller
包并单击鼠标右键,在弹出的快捷菜单中选择 “New→Java Class” 选项,在弹出的窗口中输入 “NewBeeMallGoodsController”,在 NewBeeMallGoodsController
类中新增如下代码:
package ltd.goods.cloud.newbee.controller;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import ltd.common.cloud.newbee.enums.ServiceResultEnum;
import ltd.common.cloud.newbee.dto.PageQueryUtil;
import ltd.common.cloud.newbee.dto.PageResult;
import ltd.common.cloud.newbee.dto.Result;
import ltd.common.cloud.newbee.dto.ResultGenerator;
import ltd.common.cloud.newbee.exception.NewBeeMallException;
import ltd.common.cloud.newbee.pojo.MallUserToken;
import ltd.common.cloud.newbee.util.BeanUtil;
import ltd.goods.cloud.newbee.config.annotation.TokenToMallUser;
import ltd.goods.cloud.newbee.controller.vo.NewBeeMallGoodsDetailVO;
import ltd.goods.cloud.newbee.controller.vo.NewBeeMallSearchGoodsVO;
import ltd.goods.cloud.newbee.entity.NewBeeMallGoods;
import ltd.goods.cloud.newbee.service.NewBeeMallGoodsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestHeader;
import com.vi.mall.common.Result;
import com.vi.mall.goods.service.NewBeeMallGoodsService;
import com.vi.mall.goods.controller.vo.NewBeeMallSearchGoodsVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Resource;
/**
* @author vi
* @since 2023/03/23 14:27
*/
@RestController
@Api(value = "vi", tags = "新蜂商城商品相关接口")
@RequestMapping("/goods/mall")
public class NewBeeMallGoodsController {
private static final Logger logger = LoggerFactory.getLogger(NewBeeMallGoodsController.class);
@Resource
private NewBeeMallGoodsService newBeeMallGoodsService;
@GetMapping("/search")
@ApiOperation(value = "商品搜索接口", notes = "根据关键字和分类 id 进行搜索")
public Result<PageResult<List<NewBeeMallSearchGoodsVO>>> search(@RequestParam(required = false) @ApiParam(value = "搜索关键字") String keyword,
@RequestParam(required = false) @ApiParam(value = "分类 id") Long goodsCategoryId,
@RequestParam(required = false) @ApiParam(value = "orderBy") String orderBy,
@RequestParam(required = false) @ApiParam(value = "页码") Integer pageNumber,
@RequestHeader("token") String loginMallUserToken) {
logger.info("goods search api, keyword={}, goodsCategoryId={}, orderBy={}, pageNumber={}, userId={}", keyword, goodsCategoryId, orderBy, pageNumber, loginMallUserToken.getUserId());
Map<String, Object> params = new HashMap<>(8);
//两个搜索参数为空,直接返回异常
if (goodsCategoryId == null && !StringUtils.hasText(keyword)) {
NewBeeMallException.fail("非法搜索参数");
}
if (pageNumber == null || pageNumber < 1) {
pageNumber = 1;
}
params.put("goodsCategoryId", goodsCategoryId);
params.put("page", pageNumber);
params.put("limit", 10);
//对 keyword 做过滤 去掉空格
if (StringUtils.hasText(keyword)) {
params.put("keyword", keyword);
}
if (StringUtils.hasText(orderBy)) {
params.put("orderBy", orderBy);
}
//搜索上架状态的商品
params.put("goodsSellStatus", 0);
//封装商品数据
PageQueryUtil pageUtil = new PageQueryUtil(params);
return ResultGenerator.genSuccessResult(newBeeMallGoodsService.searchNewBeeMallGoods(pageUtil));
}
@GetMapping("/detail/{goodsId}")
@ApiOperation(value = "商品详情接口", notes = "传参为商品id")
public Result<NewBeeMallGoodsDetailVO> goodsDetail(@ApiParam(value = "商品id") @PathVariable("goodsId") Long goodsId, @TokenToMallUser MallUserToken loginMallUserToken) {
logger.info("detail api,goodsId={},userId={}", goodsId, loginMallUserToken.getUserId());
if (goodsId < 1) {
return ResultGenerator.genFailResult("参数异常");
}
NewBeeMallGoods goods = newBeeMallGoodsService.getNewBeeMallGoodsById(goodsId);
if (goods == null || goods.getGoodsSellStatus() != 0) {
NewBeeMallException.fail(ServiceResultEnum.GOODS_PUT_DOWN.getResult());
}
NewBeeMallGoodsDetailVO goodsDetailVO = new NewBeeMallGoodsDetailVO();
BeanUtil.copyProperties(goods, goodsDetailVO);
goodsDetailVO.setGoodsCarouselList(Arrays.asList(goods.getGoodsCarousel().split(",")));
return ResultGenerator.genSuccessResult(goodsDetailVO);
}
}
商品列表接口的方法名称为 search()
,请求类型为 GET
,路径映射为 /goods/mall/search
,所有的传参都用 @RequestParam
注解进行接收,前端传过来的参数主要有:
-
keyword;
-
goodsCategoryId;
-
orderBy;
-
pageNumber.
参数 pageNumber
是分页所必需的字段,如果不传,则默认为第 1 页。参数 keyword
是关键字,用来过滤商品名称和商品简介。参数 goodsCategoryId
是用来过滤商品分类 id
的字段。参数 orderBy
是排序字段,传过来不同的排序方式,返回的数据也会不同。另外,还有一个参数是当前登录用户的信息,已经用 @TokenToMallUser
注解来接收,相关逻辑已经介绍过,这里不再赘述。根据以上字段进行查询参数的封装,之后通过 SQL
语句查询对应的分页数据 pageResult
。响应结果类型为 Result
,实际的 data
属性类型为 PageResult<List>
对象,即商品搜索结果的分页列表数据。
实现逻辑就是调用商品业务实现类 NewBeeMallGoodsService
中的查询方法,将所需的数据进行查询并响应给前端,所有的实现逻辑都是在业务实现类中处理的,包括查询和返回字段的内容设置。在控制层代码中,主要进行参数判断和参数的封装,以及将获得的数据结果设置给 Result
对象并返回。
商品详情接口的方法名称为 goodsDetail()
,请求类型为 GET
,路径映射为 /goods/mall/detail/{goodsId}
,参数 goodsId
就是商品主键 id
,通过 @PathVariable
注解读取路径中的这个字段值,并根据这个字段值调用 NewBeeMallGoodsService
中的 getNewBeeMallGoodsById()
方法,获取 NewBeeMallGoods
对象。getNewBeeMallGoodsById()
方法的实现方式是根据主键 id
查询数据库中的商品表并返回商品实体数据,将查询到的商品详情数据转换为 NewBeeMallGoodsDetailVO
对象并返回给前端。因为在商品表中并不是所有的字段都需要返回,所以这里做了一次对象的转换。
商品接口网关配置
打开 newbee-mall-cloud-gateway-mall 项目中的 application.properties
文件,新增关于商品微服务的路由信息,配置项为 spring.cloud.gateway.routes.*
,新增内容如下:
# 商品接口的路由配置
spring.cloud.gateway.routes[3].id=goods-service-route2
spring.cloud.gateway.routes[3].uri=lb://newbee-mall-cloud-goods-service
spring.cloud.gateway.routes[3].order=1
spring.cloud.gateway.routes[3].predicates[0]=Path=/goods/mall/**
这里配置 newbee-mall-cloud-gateway-mall
到商品微服务的路由信息,主要配置商品列表和商品详情页面的接口。如果访问商城端网关项目的路径是以 /goods/mall/
开头的,就路由到商品微服务实例。