上文回顾 上节 我们实现了仿jd的轮播广告以及商品分类的功能,并且讲解了不同的注入方式,本节我们将继续实现我们的电商主业务,商品信息的展示。 需求分析 首先,在我们开始本节编码之前,我们先来分析一下都有哪些地方会对商品进行展示,打开jd首页,鼠标下拉可以看到如下: 首页商品列表示例 可以看到,在大类型下查询了部分商品在首页进行展示(可以是最新的,也可以是网站推荐等等),然后点击任何一个分类,可以看到如下: 分类商品列表示例 我们一般进到电商网站之后,最常用的一个功能就是搜索,搜索钢琴 结果如下: 搜索查询结果示例 选择任意一个商品点击,都可以进入到详情页面,这个是单个商品的信息展示。 综上,我们可以知道,要实现一个电商平台的商品展示,最基本的包含: 首页推荐/最新上架商品 分类查询商品 关键词搜索商品 商品详情展示 ... 接下来,我们就可以开始商品相关的业务开发了。 首页商品列表|IndexProductList 开发梳理 我们首先来实现在首页展示的推荐商品列表,来看一下都需要展示哪些信息,以及如何进行展示。 商品主键(product_id) 展示图片(image_url) 商品名称(product_name) 商品价格(product_price) 分类说明(description) 分类名称(category_name) 分类主键(category_id) 其他... 编码实现 根据一级分类查询 遵循开发顺序,自下而上,如果基础mapper解决不了,那么优先编写SQL mapper,因为我们需要在同一张表中根据parent_id递归的实现数据查询,当然我们这里使用的是表链接的方式实现。因此,common mapper无法满足我们的需求,需要自定义mapper实现。 Custom Mapper实现 和上节根据一级分类查询子分类一样,在项目mscx-shop-mapper中添加一个自定义实现接口com.liferunner.custom.ProductCustomMapper,然后在resources\mapper\custom路径下同步创建xml文件mapper/custom/ProductCustomMapper.xml,此时,因为我们在上节中已经配置了当前文件夹可以被容器扫描到,所以我们添加的新的mapper就会在启动时被扫描加载,代码如下: /** * ProductCustomMapper for : 自定义商品Mapper */ public interface ProductCustomMapper { /*** * 根据一级分类查询商品 * * @param paramMap 传递一级分类(map传递多参数) * @return java.util.List */ List getIndexProductDtoList(@Param("paramMap") Map paramMap); } Service实现 在serviceproject 创建com.liferunner.service.IProductService接口以及其实现类com.liferunner.service.impl.ProductServiceImpl,添加查询方法如下: public interface IProductService { /** * 根据一级分类id获取首页推荐的商品list * * @param rootCategoryId 一级分类id * @return 商品list */ List getIndexProductDtoList(Integer rootCategoryId); ... } --- @Slf4j @Service @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class ProductServiceImpl implements IProductService { // RequiredArgsConstructor 构造器注入 private final ProductCustomMapper productCustomMapper; @Transactional(propagation = Propagation.SUPPORTS) @Override public List getIndexProductDtoList(Integer rootCategoryId) { log.info("====== ProductServiceImpl#getIndexProductDtoList(rootCategoryId) : {}=======", rootCategoryId); Map map = new HashMap<>(); map.put("rootCategoryId", rootCategoryId); val indexProductDtoList = this.productCustomMapper.getIndexProductDtoList(map); if (CollectionUtils.isEmpty(indexProductDtoList)) { log.warn("ProductServiceImpl#getIndexProductDtoList未查询到任何商品信息"); } log.info("查询结果:{}", indexProductDtoList); return indexProductDtoList; } } Controller实现 接着,在com.liferunner.api.controller.IndexController中实现对外暴露的查询接口: @RestController @RequestMapping("/index") @Api(value = "首页信息controller", tags = "首页信息接口API") @Slf4j public class IndexController { ... @Autowired private IProductService productService; @GetMapping("/rootCategorys") @ApiOperation(value = "查询一级分类", notes = "查询一级分类") public JsonResponse findAllRootCategorys() { log.info("============查询一级分类=============="); val categoryResponseDTOS = this.categoryService.getAllRootCategorys(); if (CollectionUtils.isEmpty(categoryResponseDTOS)) { log.info("============未查询到任何分类=============="); return JsonResponse.ok(Collections.EMPTY_LIST); } log.info("============一级分类查询result:{}==============", categoryResponseDTOS); return JsonResponse.ok(categoryResponseDTOS); } ... } Test API 编写完成之后,我们需要对我们的代码进行测试验证,还是通过使用RestService插件来实现,当然,大家也可以通过Postman来测试,结果如下: 根据一级分类查询商品列表 商品列表|ProductList 如开文之初我们看到的京东商品列表一样,我们先分析一下在商品列表页面都需要哪些元素信息? 开发梳理 商品列表的展示按照我们之前的分析,总共分为2大类: 选择商品分类之后,展示当前分类下所有商品 输入搜索关键词后,展示当前搜索到相关的所有商品 在这两类中展示的商品列表数据,除了数据来源不同以外,其他元素基本都保持一致,那么我们是否可以使用统一的接口来根据参数实现隔离呢? 理论上不存在问题,完全可以通过传参判断的方式进行数据回传,但是,在我们实现一些可预见的功能需求时,一定要给自己的开发预留后路,也就是我们常说的可拓展性,基于此,我们会分开实现各自的接口,以便于后期的扩展。 接着来分析在列表页中我们需要展示的元素,首先因为需要分上述两种情况,因此我们需要在我们API设计的时候分别处理,针对于 1.分类的商品列表展示,需要传入的参数有: 分类id 排序(在电商列表我们常见的几种排序(销量,价格等等)) 分页相关(因为我们不可能把数据库中所有的商品都取出来) PageNumber(当前第几页) PageSize(每页显示多少条数据) 2.关键词查询商品列表,需要传入的参数有: 关键词 排序(在电商列表我们常见的几种排序(销量,价格等等)) 分页相关(因为我们不可能把数据库中所有的商品都取出来) PageNumber(当前第几页) PageSize(每页显示多少条数据) 需要在页面展示的信息有: 商品id(用于跳转商品详情使用) 商品名称 商品价格 商品销量 商品图片 商品优惠 ... 编码实现 根据上面我们的分析,接下来开始我们的编码: 根据商品分类查询 根据我们的分析,肯定不会在一张表中把所有数据获取全,因此我们需要进行多表联查,故我们需要在自定义mapper中实现我们的功能查询. ResponseDTO 实现 根据我们前面分析的前端需要展示的信息,我们来定义一个用于展示这些信息的对象com.liferunner.dto.SearchProductDTO,代码如下: @Data @NoArgsConstructor @AllArgsConstructor @Builder public class SearchProductDTO { private String productId; private String productName; private Integer sellCounts; private String imgUrl; private Integer priceDiscount; //商品优惠,我们直接计算之后返回优惠后价格 } Custom Mapper 实现 在com.liferunner.custom.ProductCustomMapper.java中新增一个方法接口: List searchProductListByCategoryId(@Param("paramMap") Map paramMap); 同时,在mapper/custom/ProductCustomMapper.xml中实现我们的查询方法: 主要来说明一下这里的模块,以及为什么不使用if标签。 在有的时候,我们并不希望所有的条件都同时生效,而只是想从多个选项中选择一个,但是在使用IF标签时,只要test中的表达式为 true,就会执行IF 标签中的条件。MyBatis 提供了 choose 元素。IF标签是与(and)的关系,而 choose 是或(or)的关系。 它的选择是按照顺序自上而下,一旦有任何一个满足条件,则选择退出。 Service 实现 然后在servicecom.liferunner.service.IProductService中添加方法接口: /** * 根据商品分类查询商品列表 * * @param categoryId 分类id * @param sortby 排序方式 * @param pageNumber 当前页码 * @param pageSize 每页展示多少条数据 * @return 通用分页结果视图 */ CommonPagedResult searchProductList(Integer categoryId, String sortby, Integer pageNumber, Integer pageSize); 在实现类com.liferunner.service.impl.ProductServiceImpl中,实现上述方法: // 方法重载 @Override public CommonPagedResult searchProductList(Integer categoryId, String sortby, Integer pageNumber, Integer pageSize) { Map paramMap = new HashMap<>(); paramMap.put("categoryId", categoryId); paramMap.put("sortby", sortby); // mybatis-pagehelper PageHelper.startPage(pageNumber, pageSize); val searchProductDTOS = this.productCustomMapper.searchProductListByCategoryId(paramMap); // 获取mybatis插件中获取到信息 PageInfo pageInfo = new PageInfo<>(searchProductDTOS); // 封装为返回到前端分页组件可识别的视图 val commonPagedResult = CommonPagedResult.builder() .pageNumber(pageNumber) .rows(searchProductDTOS) .totalPage(pageInfo.getPages()) .records(pageInfo.getTotal()) .build(); return commonPagedResult; } 在这里,我们使用到了一个mybatis-pagehelper插件,会在下面的福利讲解中分解。 Controller 实现 继续在com.liferunner.api.controller.ProductController中添加对外暴露的接口API: @GetMapping("/searchByCategoryId") @ApiOperation(value = "查询商品信息列表", notes = "根据商品分类查询商品列表") public JsonResponse searchProductListByCategoryId( @ApiParam(name = "categoryId", value = "商品分类id", required = true, example = "0") @RequestParam Integer categoryId, @ApiParam(name = "sortby", value = "排序方式", required = false) @RequestParam String sortby, @ApiParam(name = "pageNumber", value = "当前页码", required = false, example = "1") @RequestParam Integer pageNumber, @ApiParam(name = "pageSize", value = "每页展示记录数", required = false, example = "10") @RequestParam Integer pageSize ) { if (null == categoryId || categoryId == 0) { return JsonResponse.errorMsg("分类id错误!"); } if (null == pageNumber || 0 == pageNumber) { pageNumber = DEFAULT_PAGE_NUMBER; } if (null == pageSize || 0 == pageSize) { pageSize = DEFAULT_PAGE_SIZE; } log.info("============根据分类:{} 搜索列表==============", categoryId); val searchResult = this.productService.searchProductList(categoryId, sortby, pageNumber, pageSize); return JsonResponse.ok(searchResult); } 因为我们的请求中,只会要求商品分类id是必填项,其余的调用方都可以不提供,但是如果不提供的话,我们系统就需要给定一些默认的参数来保证我们的系统正常稳定的运行,因此,我定义了com.liferunner.api.controller.BaseController,用于存储一些公共的配置信息。 /** * BaseController for : controller 基类 */ @Controller public class BaseController { /** * 默认展示第1页 */ public final Integer DEFAULT_PAGE_NUMBER = 1; /** * 默认每页展示10条数据 */ public final Integer DEFAULT_PAGE_SIZE = 10; } Test API 测试的参数分别是:categoryId : 51 ,sortby : price,pageNumber : 1,pageSize : 5 根据分类id查询 可以看到,我们查询到7条数据,总页数totalPage为2,并且根据价格从小到大进行了排序,证明我们的编码是正确的。接下来,通过相同的代码逻辑,我们继续实现根据搜索关键词进行查询。 根据关键词查询 Response DTO 实现 使用上面实现的com.liferunner.dto.SearchProductDTO. Custom Mapper 实现 在com.liferunner.custom.ProductCustomMapper中新增方法: List searchProductList(@Param("paramMap") Map paramMap); 在mapper/custom/ProductCustomMapper.xml中添加查询SQL: Service 实现 在com.liferunner.service.IProductService中新增查询接口: /** * 查询商品列表 * * @param keyword 查询关键词 * @param sortby 排序方式 * @param pageNumber 当前页码 * @param pageSize 每页展示多少条数据 * @return 通用分页结果视图 */ CommonPagedResult searchProductList(String keyword, String sortby, Integer pageNumber, Integer pageSize); 在com.liferunner.service.impl.ProductServiceImpl实现上述接口方法: @Override public CommonPagedResult searchProductList(String keyword, String sortby, Integer pageNumber, Integer pageSize) { Map paramMap = new HashMap<>(); paramMap.put("keyword", keyword); paramMap.put("sortby", sortby); // mybatis-pagehelper PageHelper.startPage(pageNumber, pageSize);