[springboot 开发单体web shop] 7. 多种形式提供商品列表

上文回顾

上节 咱们实现了仿jd的轮播广告以及商品分类的功能,而且讲解了不一样的注入方式,本节咱们将继续实现咱们的电商主业务,商品信息的展现。前端

需求分析

首先,在咱们开始本节编码以前,咱们先来分析一下都有哪些地方会对商品进行展现,打开jd首页,鼠标下拉能够看到以下:
首页商品列表示例java

能够看到,在大类型下查询了部分商品在首页进行展现(能够是最新的,也能够是网站推荐等等),而后点击任何一个分类,能够看到以下:
分类商品列表示例mysql

咱们通常进到电商网站以后,最经常使用的一个功能就是搜索,搜索钢琴) 结果以下:
搜索查询结果示例git

选择任意一个商品点击,均可以进入到详情页面,这个是单个商品的信息展现。
综上,咱们能够知道,要实现一个电商平台的商品展现,最基本的包含:github

  • 首页推荐/最新上架商品
  • 分类查询商品
  • 关键词搜索商品
  • 商品详情展现
  • ...

接下来,咱们就能够开始商品相关的业务开发了。spring

首页商品列表|IndexProductList

开发梳理

咱们首先来实如今首页展现的推荐商品列表,来看一下都须要展现哪些信息,以及如何进行展现。sql

  • 商品主键(product_id)
  • 展现图片(image_url)
  • 商品名称(product_name)
  • 商品价格(product_price)
  • 分类说明(description)
  • 分类名称(category_name)
  • 分类主键(category_id)
  • 其余...

编码实现

根据一级分类查询

遵循开发顺序,自下而上,若是基础mapper解决不了,那么优先编写SQL mapper,由于咱们须要在同一张表中根据parent_id递归的实现数据查询,固然咱们这里使用的是表连接的方式实现。所以,common mapper没法知足咱们的需求,须要自定义mapper实现。shell

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<com.liferunner.dto.IndexProductDTO>
     */
    List<IndexProductDTO> getIndexProductDtoList(@Param("paramMap") Map<String, Integer> paramMap);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.liferunner.custom.ProductCustomMapper">
    <resultMap id="IndexProductDTO" type="com.liferunner.dto.IndexProductDTO">
        <id column="rootCategoryId" property="rootCategoryId"/>
        <result column="rootCategoryName" property="rootCategoryName"/>
        <result column="slogan" property="slogan"/>
        <result column="categoryImage" property="categoryImage"/>
        <result column="bgColor" property="bgColor"/>
        <collection property="productItemList" ofType="com.liferunner.dto.IndexProductItemDTO">
            <id column="productId" property="productId"/>
            <result column="productName" property="productName"/>
            <result column="productMainImageUrl" property="productMainImageUrl"/>
            <result column="productCreateTime" property="productCreateTime"/>
        </collection>
    </resultMap>
    <select id="getIndexProductDtoList" resultMap="IndexProductDTO" parameterType="Map">
        SELECT
        c.id as rootCategoryId,
        c.name as rootCategoryName,
        c.slogan as slogan,
        c.category_image as categoryImage,
        c.bg_color as bgColor,
        p.id as productId,
        p.product_name as productName,
        pi.url as productMainImageUrl,
        p.created_time as productCreateTime
        FROM category c
        LEFT JOIN products p
        ON c.id = p.root_category_id
        LEFT JOIN products_img pi
        ON p.id = pi.product_id
        WHERE c.type = 1
        AND p.root_category_id = #{paramMap.rootCategoryId}
        AND pi.is_main = 1
        LIMIT 0,10;
    </select>
</mapper>

Service实现

serviceproject 建立com.liferunner.service.IProductService接口以及其实现类com.liferunner.service.impl.ProductServiceImpl,添加查询方法以下:apache

public interface IProductService {

    /**
     * 根据一级分类id获取首页推荐的商品list
     *
     * @param rootCategoryId 一级分类id
     * @return 商品list
     */
    List<IndexProductDTO> 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<IndexProductDTO> getIndexProductDtoList(Integer rootCategoryId) {
        log.info("====== ProductServiceImpl#getIndexProductDtoList(rootCategoryId) : {}=======", rootCategoryId);
        Map<String, Integer> 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<SearchProductDTO> searchProductListByCategoryId(@Param("paramMap") Map<String, Object> paramMap);

同时,在mapper/custom/ProductCustomMapper.xml中实现咱们的查询方法:

<select id="searchProductListByCategoryId" resultType="com.liferunner.dto.SearchProductDTO" parameterType="Map">
        SELECT
        p.id as productId,
        p.product_name as productName,
        p.sell_counts as sellCounts,
        pi.url as imgUrl,
        tp.priceDiscount
        FROM products p
        LEFT JOIN products_img pi
        ON p.id = pi.product_id
        LEFT JOIN
        (
        SELECT product_id, MIN(price_discount) as priceDiscount
        FROM products_spec
        GROUP BY product_id
        ) tp
        ON tp.product_id = p.id
        WHERE pi.is_main = 1
        AND p.category_id = #{paramMap.categoryId}
        ORDER BY
        <choose>
            <when test="paramMap.sortby != null and paramMap.sortby == 'sell'">
                p.sell_counts DESC
            </when>
            <when test="paramMap.sortby != null and paramMap.sortby == 'price'">
                tp.priceDiscount ASC
            </when>
            <otherwise>
                p.created_time DESC
            </otherwise>
        </choose>
    </select>

主要来讲明一下这里的<choose>模块,以及为何不使用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<String, Object> 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<SearchProductDTO> searchProductList(@Param("paramMap") Map<String, Object> paramMap);

mapper/custom/ProductCustomMapper.xml中添加查询SQL:

<select id="searchProductList" resultType="com.liferunner.dto.SearchProductDTO" parameterType="Map">
        SELECT
        p.id as productId,
        p.product_name as productName,
        p.sell_counts as sellCounts,
        pi.url as imgUrl,
        tp.priceDiscount
        FROM products p
        LEFT JOIN products_img pi
        ON p.id = pi.product_id
        LEFT JOIN
        (
        SELECT product_id, MIN(price_discount) as priceDiscount
        FROM products_spec
        GROUP BY product_id
        ) tp
        ON tp.product_id = p.id
        WHERE pi.is_main = 1
        <if test="paramMap.keyword != null and paramMap.keyword != ''">
            AND p.item_name LIKE "%${paramMap.keyword}%"
        </if>
        ORDER BY
        <choose>
            <when test="paramMap.sortby != null and paramMap.sortby == 'sell'">
                p.sell_counts DESC
            </when>
            <when test="paramMap.sortby != null and paramMap.sortby == 'price'">
                tp.priceDiscount ASC
            </when>
            <otherwise>
                p.created_time DESC
            </otherwise>
        </choose>
    </select>

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<String, Object> paramMap = new HashMap<>();
        paramMap.put("keyword", keyword);
        paramMap.put("sortby", sortby);
        // mybatis-pagehelper
        PageHelper.startPage(pageNumber, pageSize);
        val searchProductDTOS = this.productCustomMapper.searchProductList(paramMap);
        // 获取mybatis插件中获取到信息
        PageInfo<?> pageInfo = new PageInfo<>(searchProductDTOS);
        // 封装为返回到前端分页组件可识别的视图
        val commonPagedResult = CommonPagedResult.builder()
                .pageNumber(pageNumber)
                .rows(searchProductDTOS)
                .totalPage(pageInfo.getPages())
                .records(pageInfo.getTotal())
                .build();
        return commonPagedResult;
    }

上述方法和以前searchProductList(Integer categoryId, String sortby, Integer pageNumber, Integer pageSize) 惟一的区别就是它是确定搜索关键词来进行数据查询,使用重载的目的是为了咱们后续不一样类型的业务扩展而考虑的。

Controller 实现

com.liferunner.api.controller.ProductController中添加关键词搜索API:

@GetMapping("/search")
    @ApiOperation(value = "查询商品信息列表", notes = "查询商品信息列表")
    public JsonResponse searchProductList(
        @ApiParam(name = "keyword", value = "搜索关键词", required = true)
        @RequestParam String keyword,
        @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 (StringUtils.isBlank(keyword)) {
            return JsonResponse.errorMsg("搜索关键词不能为空!");
        }
        if (null == pageNumber || 0 == pageNumber) {
            pageNumber = DEFAULT_PAGE_NUMBER;
        }
        if (null == pageSize || 0 == pageSize) {
            pageSize = DEFAULT_PAGE_SIZE;
        }
        log.info("============根据关键词:{} 搜索列表==============", keyword);

        val searchResult = this.productService.searchProductList(keyword, sortby, pageNumber, pageSize);
        return JsonResponse.ok(searchResult);
    }

Test API

测试参数:keyword : 西凤,sortby : sell,pageNumber : 1,pageSize : 10
测试关键词结果
根据销量排序正常,查询关键词正常,总条数32,每页10条,总共3页正常。

福利讲解

在本节编码实现中,咱们使用到了一个通用的mybatis分页插件mybatis-pagehelper,接下来,咱们来了解一下这个插件的基本状况。

mybatis-pagehelper

若是各位小伙伴使用过:MyBatis 分页插件 PageHelper, 那么对于这个就很容易理解了,它其实就是基于Executor 拦截器来实现的,当拦截到原始SQL以后,对SQL进行一次改造处理。
咱们来看看咱们本身代码中的实现,根据springboot编码三部曲:

1.添加依赖

<!-- 引入mybatis-pagehelper 插件-->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.12</version>
        </dependency>

有同窗就要问了,为何引入的这个依赖和我原来使用的不一样?之前使用的是:

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.1.10</version>
</dependency>

答案就在这里:依赖传送门
spring-boot-pagehelper
咱们使用的是springboot进行的项目开发,既然使用的是springboot,那咱们彻底能够用到它的自动装配特性,做者帮咱们实现了这么一个自动装配的jar,咱们只须要参考示例来编写就ok了。

2.改配置

# mybatis 分页组件配置
pagehelper:
  helperDialect: mysql #插件支持12种数据库,选择类型
  supportMethodsArguments: true

3.改代码

以下示例代码:

@Override
    public CommonPagedResult searchProductList(String keyword, String sortby, Integer pageNumber, Integer pageSize) {
        Map<String, Object> paramMap = new HashMap<>();
        paramMap.put("keyword", keyword);
        paramMap.put("sortby", sortby);
        // mybatis-pagehelper
        PageHelper.startPage(pageNumber, pageSize);
        val searchProductDTOS = this.productCustomMapper.searchProductList(paramMap);
        // 获取mybatis插件中获取到信息
        PageInfo<?> pageInfo = new PageInfo<>(searchProductDTOS);
        // 封装为返回到前端分页组件可识别的视图
        val commonPagedResult = CommonPagedResult.builder()
                .pageNumber(pageNumber)
                .rows(searchProductDTOS)
                .totalPage(pageInfo.getPages())
                .records(pageInfo.getTotal())
                .build();
        return commonPagedResult;
    }

在咱们查询数据库以前,咱们引入了一句PageHelper.startPage(pageNumber, pageSize);,告诉mybatis咱们要对查询进行分页处理,这个时候插件会启动一个拦截器com.github.pagehelper.PageInterceptor,针对全部的query进行拦截,添加自定义参数和添加查询数据总数。(后续咱们会打印sql来证实。)

当查询到结果以后,咱们须要将咱们查询到的结果通知给插件,也就是PageInfo<?> pageInfo = new PageInfo<>(searchProductDTOS);com.github.pagehelper.PageInfo是对插件针对分页作的一个属性包装,具体能够查看属性传送门)。

至此,咱们的插件使用就已经结束了。可是为何咱们在后面又封装了一个对象来对外进行返回,而不是使用查询到的PageInfo呢?这是由于咱们实际开发过程当中,为了数据结构的一致性作的一次结构封装,你也可不实现该步骤,都是对结果没有任何影响的。

SQL打印对比

2019-11-21 12:04:21 INFO  ProductController:134 - ============根据关键词:西凤 搜索列表==============
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4ff449ba] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1980420239 wrapping com.mysql.cj.jdbc.ConnectionImpl@563b22b1] will not be managed by Spring
==>  Preparing: SELECT count(0) FROM products p LEFT JOIN products_img pi ON p.id = pi.product_id LEFT JOIN (SELECT product_id, MIN(price_discount) AS priceDiscount FROM products_spec GROUP BY product_id) tp ON tp.product_id = p.id WHERE pi.is_main = 1 AND p.product_name LIKE "%西凤%" 
==> Parameters: 
<==    Columns: count(0)
<==        Row: 32
<==      Total: 1
==>  Preparing: SELECT p.id as productId, p.product_name as productName, p.sell_counts as sellCounts, pi.url as imgUrl, tp.priceDiscount FROM product p LEFT JOIN products_img pi ON p.id = pi.product_id LEFT JOIN ( SELECT product_id, MIN(price_discount) as priceDiscount FROM products_spec GROUP BY product_id ) tp ON tp.product_id = p.id WHERE pi.is_main = 1 AND p.product_name LIKE "%西凤%" ORDER BY p.sell_counts DESC LIMIT ? 
==> Parameters: 10(Integer)

咱们能够看到,咱们的SQL中多了一个SELECT count(0),第二条SQL多了一个LIMIT参数,在代码中,咱们很明确的知道,咱们并无显示的去搜索总数和查询条数,能够肯定它就是插件帮咱们实现的。

源码下载

Github 传送门
Gitee 传送门

下节预告

下一节咱们将继续开发商品详情展现以及商品评价业务,在过程当中使用到的任何开发组件,我都会经过专门的一节来进行介绍的,兄弟们末慌!

gogogo!

相关文章
相关标签/搜索