up:: SpringBoot电商项目商品模块商品详情接口

说明:

(1) 本篇博客比较重要的点:

●【参数没有放在body中,而是放在了url中】+【后端使用实体类去接收参数】:对于这种情况,不能使用@RequestBody注解,自然也不能使用@RequestParam注解;(只是,我们在写实体类的时候,实体类的属性要比照着接口参数写,不能写错)

● 对于那些条件比较多的接口,专门创建一个Query对象,来组织条件;是一个不错的开发技巧;

● 对于排序方式这种查询条件,我们最好根据接口要求,在程序中规定好【究竟有哪些排序条件】;这能很好的防止接口乱调用,程序也更加安全;

● 本篇博客也涉及到了【mybatis动态SQL,<if></if>】、【MySQL的模糊查询,like】、【MySQL的in子句】、【mybatis的批处理中的<foreach>子标签】等;

(2) 本篇博客一个待进一步研究的地方:这人的查询,我们使用的是MySQL的原生查询方式;但是,如果以后我们的项目和大数据结合的时候,似乎可以了解一下ElasticSearch这些技术,来更好的实现数据的检索和查询;


一:前台的【商品列表】接口说明;

1.前台的【商品列表】接口,在界面上的表现;

(1)我们可以不使用任何条件,来分页显示商品数据;


(2)我们可以使用【按价格从低到高或从高到低排序】这个条件,来分页显示商品数据;


(3)我们可以使用【商品名需要包含某个关键词】这个条件,来分页显示商品数据;与此同时,也可以加入【按价格从低到高或从高到低排序】这个条件;


(4)我们可以使用【商品需要隶属于“某商品分类及其子分类”】这个条件,来分页显示商品数据;与此同时,也可以加入【按价格从低到高或从高到低排序】这个条件;

PS:上图少标注了一个,id=19的果冻橙这个分类,是id=4的橘子橙子这个分类的子目录;;;所以,id=36的四川果冻橙要需要查询出来;


可以看到,前台的【商品列表】接口,这一个接口需要承担的任务还是挺多的;这就需要我们在开发这个接口时,做好条件判断和处理;

2.前台的【商品列表】接口文档;


以这个请求实例为例,来仔细说下前台的【商品列表】接口文档;

接口返回内容:

{
    "status": 10000,
    "msg": "SUCCESS",
    "data": {
        "total": 3,
        "list": [
            {
                "id": 24,
                "name": "智利帝王蟹礼盒装4.4-4.0斤/只 生鲜活鲜熟冻大螃蟹",
                "image": "http://111.231.103.117:8081/images/diwangxie.jpg",
                "detail": "商品毛重:3.0kg商品产地:智利大闸蟹售卖方式:公蟹重量:2000-4999g套餐份量:5人份以上国产/进口:进口海水/淡水:海水烹饪建议:火锅,炒菜,烧烤,刺身,加热即食包装:简装原产地:智利保存状态:冷冻公单蟹重:5.5两及以上分类:帝王蟹特产品类:其它售卖方式:单品",
                "categoryId": 7,
                "price": 222,
                "stock": 222,
                "status": 1,
                "createTime": "2019-12-28T08:06:34.000+0000",
                "updateTime": "2020-02-10T16:05:05.000+0000"
            },
            {
                "id": 21,
                "name": "智利原味三文鱼排(大西洋鲑)240g/袋 4片装",
                "image": "http://111.231.103.117:8081/images/sanwenyu2.jpg",
                "detail": "商品毛重:260.00g商品产地:中国大陆保存状态:冷冻国产/进口:进口包装:简装类别:三文鱼海水/淡水:海水烹饪建议:煎炸,蒸菜,烧烤原产地:智利",
                "categoryId": 8,
                "price": 499,
                "stock": 1,
                "status": 1,
                "createTime": "2019-12-28T07:13:07.000+0000",
                "updateTime": "2020-02-10T15:38:46.000+0000"
            },
            {
                "id": 22,
                "name": "即食海参大连野生辽刺参 新鲜速食 特级生鲜海产 60~80G",
                "image": "http://111.231.103.117:8081/images/haishen.jpg",
                "detail": "商品毛重:1.5kg商品产地:中国大陆贮存条件:冷冻重量:50-99g国产/进口:国产适用场景:养生滋补包装:袋装原产地:辽宁年限:9年以上等级:特级食品工艺:冷冻水产热卖时间:9月类别:即食海参固形物含量:70%-90%特产品类:大连海参售卖方式:单品",
                "categoryId": 13,
                "price": 699,
                "stock": 3,
                "status": 1,
                "createTime": "2019-12-28T07:16:29.000+0000",
                "updateTime": "2020-02-10T16:04:29.000+0000"
            }
        ],
        "pageNum": 1,
        "pageSize": 10,
        "size": 3,
        "startRow": 1,
        "endRow": 3,
        "pages": 1,
        "prePage": 0,
        "nextPage": 0,
        "isFirstPage": true,
        "isLastPage": true,
        "hasPreviousPage": false,
        "hasNextPage": false,
        "navigatePages": 8,
        "navigatepageNums": [
            1
        ],
        "navigateFirstPage": 1,
        "navigateLastPage": 1
    }
}

这儿主要说明两点:

● 这儿显示的商品都是扁平的,没有嵌套;

● 根据接口文档对返回数据的格式要求,我们可以发现,需要使用PageHelper分页组件的PageInfo对象来组织返回的分页数据;

3. 前台的【商品列表】接口,开发分析;

说明:

(1) 前台的【商品列表】接口,这儿主要的地方就是根据不同的条件,去搜索的功能;

(2) 入参判空;判断参数是否为空;

● orderBy、categoryId、keyword这三个参数,可传可不传;如果传的话,我们才会使用这个参数,把这个参数作为条件去查询;

● 所以,对于orderBy、categoryId、keyword这三个参数的判空:就不使用@Valid注解的Validation参数校验的方式,来判空了;

(3) 如果传了keyword这个参数,比如传了“桃”,就表示我们搜索所有商品名中有“桃”的商品;对于这种模糊搜索的功能,我们使用%通配符和like关键字,去实现模糊查找;

● 在查找的时候,我们会使用【%通配符】和【like关键字】,在SQL中进行拼接,实现查找功能;


二:正式开发;

1.在ProductController类中,创建查询前台的商品列表的方法:list()方法;

 
         /**
          * 前台的商品列表接口;
          *
          * @return
          */
         @ApiOperation("前台商品列表")
         @GetMapping("/product/list")
         @ResponseBody
         public ApiRestResponse list(ProductListReq productListReq) {
             PageInfo pageInfoList = productService.list(productListReq);
             return ApiRestResponse.success(pageInfoList);
         }

说明:

(1)因为前台的【商品列表】接口的参数很多,所以我们创建了一个实体类ProductListReq类,用这个实体类来接收参数;

ProductListReq类:

 
 
     package com.imooc.mall.model.request;
 
     import io.swagger.models.auth.In;
 
     /**
      * 在开发【前台的商品列表】接口时,用该类的对象接收参数
      */
     public class ProductListReq {
 
         private String orderBy;//排序方式
         private Integer categoryId;//商品分类Id
         private String keyword;//搜索关键词
         private Integer pageNum = 1;//页数
         private Integer pageSize = 10;//每页条数
 
         public String getOrderBy() {
             return orderBy;
         }
 
         public void setOrderBy(String orderBy) {
             this.orderBy = orderBy;
         }
 
         public Integer getCategoryId() {
             return categoryId;
         }
 
         public void setCategoryId(Integer categoryId) {
             this.categoryId = categoryId;
         }
 
         public String getKeyword() {
             return keyword;
         }
 
         public void setKeyword(String keyword) {
             this.keyword = keyword;
         }
 
         public Integer getPageNum() {
             return pageNum;
         }
 
         public void setPageNum(Integer pageNum) {
             this.pageNum = pageNum;
         }
 
         public Integer getPageSize() {
             return pageSize;
         }
 
         public void setPageSize(Integer pageSize) {
             this.pageSize = pageSize;
         }
     }

● 根据前台的【商品列表】接口的参数,编写ProductListReq类,并且这儿给pageNum和pageSize设了默认值;

● 因为,接口是Get请求,即不是【Post请求,且把参数放在Body中】的情况;所以,这儿使用实体类接收参数的时候,是不能使用@RequestBody注解(这个注解是在,POST请求,且参数是放在Body中,而且后端我们使用实体类去接收参数时:才使用的)的;然后,又因为我们这儿是使用实体类去接收参数,所以也不能使用@RequestParam注解;所以,这儿我们使用实体类去接收参数的时候,实体类的属性一定要和接口的参数名写一样,别写错了;


(2)请求方式,url,接收参数,要写对;


(3)因为根据接口文档中对接口返回数据格式的要求,我们需要使用PageHelper的PageInfo来包装分页数据,所以我们在开发Service层的方法时,其返回类型就是PageInfo格式的;


(4)Service层的list方法,在下一部分介绍;

2.1.在ProductServiceImpl类中,创建以分页的方式查询符合条件的商品的方法:list()方法;(重点!!!)

在ProductServiceImpl类中,创建查询前台的商品列表的方法:list()方法;

 
 /**
     * 根据条件,以分页的方式,查询商品数据;
     * @param productListReq
     * @return
     */
    @Override
    public PageInfo list(ProductListReq productListReq) {
        //先构建一个专门助力于查询的Query对象
        ProductListQuery productListQuery = new ProductListQuery();
 
        //搜索条件处理
        //如果前端传了keyword这个参数;(其实,也就是前端搜索关键词了):就把这个条件赋值到productListQuery查询对象上去;
         if (!StringUtils.isEmpty(productListReq.getKeyword())) {
             //根据keyword拼凑"%keyword%",以好在数据库中进行模糊查询;
             String keyword = new StringBuilder().append("%").append(productListReq.getKeyword()).append("%").toString();
             //然后,把这个在查询数据库时,可以直接使用的"%keyword%"查询条件,赋值到productListQuery查询对象上去;
             productListQuery.setKeyword(keyword);
        }
        //如果前端传了categoryId这个参数;(其实,也就是前端选择某个商品目录):就把这个条件赋值到productListQuery查询对象上去;
        if (productListReq.getCategoryId() != null) {
            //目录处理:如果查询某个目录下的商品,不仅要查询隶属于该目录下的商品,也要查询隶属于该目录的子目录下的商品;
            //所以,先调用以前编写的一个【递归查询所有子目录】的方法;获取当前目录和当前目录的所有子目录的递归查询结果;
            List<CategoryVO> categoryVOList = categoryService.listCategoryForCustomer(productListReq.getCategoryId());
            //但是,上面的categoryVOList是一个嵌套的结果,我们要想获取当前目录和其子目录的CategoryId的集合,还需要做以下处理;
            //首先,创建一个List用来存放所有的CategoryId
            List<Integer> categoryIds = new ArrayList<>();
            //很自然,当前传参的这个CategoryId肯定在需要添加到集合中;
            categoryIds.add(productListReq.getCategoryId());
            //编写一个【遍历categoryVOList这种递归结构的数据,以获取所有categoryId的,工具方法】
            getCategoryIds(categoryVOList, categoryIds);
            //然后,把这个在查询数据库时,【当前目录和当前目录所有子目录的categoryIds】的查询条件,赋值到productListQuery查询对象上去;
            productListQuery.setCategoryIds(categoryIds);
        }
 
        //排序条件的处理
        //首先,尝试从productListReq这个传递参数中,获取排序的参数
        String orderBy = productListReq.getOrderBy();
        //如果我们从前端请求中的参数中,有orderBy这个有关排序的参数;;如果这个有关排序的参数,在我们预设的排序条件中的话;
        if (Constant.ProductListOrderBy.PRICE_ASC_DESC.contains(orderBy)) {
            PageHelper.startPage(productListReq.getPageNum(), productListReq.getPageSize(), orderBy);
        } else {
            //如果前端没有传orderBy参数,或者,传递orderBy参数的值不符合我们在【Constant.ProductListOrderBy.PRICE_ASC_DESC】中定义的格式
            PageHelper.startPage(productListReq.getPageNum(), productListReq.getPageSize());
        }
 
 
        //调用Dao层编写的(可能有条件的)查询语句
        List<Product> productList = productMapper.selectList(productListQuery);
        PageInfo pageInfo = new PageInfo(productList);
        return pageInfo;
    }
 
    /**
     * 工具方法,遍历【List<CategoryVO> categoryVOList】这种递归嵌套的数据结构,获取其中所有的categoryId;
     * @param categoryVOList
     * @param categoryIds
     */
    private void getCategoryIds(List<CategoryVO> categoryVOList, List<Integer> categoryIds) {
        //遍历传过来的这个【递归嵌套接口的,CategoryVOList】
        for (int i = 0; i < categoryVOList.size(); i++) {
            CategoryVO categoryVO =  categoryVOList.get(i);
            if (categoryVO != null) {
                categoryIds.add(categoryVO.getId());
 
                //递归调用
                getCategoryIds(categoryVO.getChildCategory(), categoryIds);
            }
 
        }
    }

说明:

(1.1)【categoryId,keyword】这两个查询条件处理:

构建一个专门用于组织查询条件的Query对象:ProductListQuery类:用Query对象来组织查询条件,代码会更加有条理,也方便以后的扩展;(这个开发技巧,比较重要!!!)

● 因为,根据接口文档和项目的实际情况,可以看到前台的【商品列表】接口在查询商品时候,其查询条件可能存在以下几种情况:(PS:这儿的条件种类,其实还不算太多;以后可能遇到条件种类很多的情况,到那时,专门构建一个Query对象的好处,会更加明显)

● 创建ProductListQuery类;

 
     package com.imooc.mall.model.query;
 
     import java.util.List;
 
     /**
      * 描述:查询商品列表的Query
      */
     public class ProductListQuery {
 
         private String keyword;//搜索关键词,这个条件
         private List<Integer> categoryIds;//商品分类,这个条件
 
         public String getKeyword() {
             return keyword;
         }
 
         public void setKeyword(String keyword) {
             this.keyword = keyword;
         }
 
         public List<Integer> getCategoryIds() {
             return categoryIds;
         }
 
         public void setCategoryIds(List<Integer> categoryIds) {
             this.categoryIds = categoryIds;
         }
     }

说明:

(1) 该Query类的第一个属性,就是keyword,其对应条件中的搜索关键词;

(2) 该Query类的第二个属性,是一个List集合,这里面是:当接口传了categoryId这个商品分类的条件后:所有符合条件的商品的category_id;

也就是说:【如果在界面上,我们点击了“新鲜水果”后】→【就相当于我们传了categoryId=3,这个参数】→【就表示,我们此时加入了categoryId=3,这个搜索条件】→【这个条件经过处理,等到具体查product表的时候,就变成了:category_id在{3,4,11,12,14,19,28}的商品了】; 其实,这样一看,这个查询条件还是比较复杂的,由这儿也能感受到【专门构建查询条件的Query对象】的好处;

● 即,我们根据前端传过来的条件,把这些条件进行处理、分析,然后构建一个Query对象,然后再以这个Query对象作为条件,去数据库查询我们最终需要的结果;这是一个十分不错的、条理清晰的策略;


(1.2) 【categoryId,keyword】这两个查询条件处理:

构建Query对象之:判断是否有【搜索关键词】这个条件:如果有,就把这个条件构建到Query对象上去;

● 这儿使用了Spring提供的一个判空工具:StringUtils.isEmpty();

● 因为,搜索关键词时:比如我们搜索“冰”,是要查询商品名中包含“冰”是商品;所以,在查数据库的时候,需要是模糊查找;所以,这儿,我们要先拼凑一个模糊查找要用的字符串;(PS:有关MySQL模糊查询,可以参考【WHERE子句条件查询】);


(1.3) 【categoryId,keyword】这两个查询条件处理:

构建Query对象之:判断是否有【商品分类Id】这个条件:如果有,就把这个条件处理一下,然后构建到Query对象上去;

● 开发前台的【分类列表(递归)】接口时,创建的:递归查询分类目录的方法listCategoryForCustomer()方法回顾;如果忘记了,可以回去看下【SpringBoot电商项目商品分类模块前台的分类列表递归接口】;

● listCategoryForCustomer()方法在此处使用的说明;

●【商品分类Id】这个条件的:处理流程分析;

由此,也可以发现,当接口传入了一个条件后,我们有的时候是无法直接用这个条件去查数据库;;;而是,要先处理下这个条件,把其处理成合适的东西,然后再那这个处理后的东西作为条件去查数据库; 具体来说,就是构建查询对象;;;这会使得程序结构更加条理清晰、有助于后期的功能扩展;

●工具方法:getCategoryIds()分析;这个方法很简单,没什么好说的;


至此,接口可能的条件:keyword和categoryId都已经构建到ProductListQuery这个Query对象上去了;

疑问:我们接口还有一个可能的条件就是排序方式,为什么这个条件不也构建到ProductListQuery这个Query对象上去呐?

这是因为,orderBy是有关排序的一个条件;我们这儿是使用PageHelper这个分页插件来实现分页查询;而,PageHelper在构建的时候,就可以设置排序方式,即我们可以通过PageHelper来应用这个条件;所以,就没有把其构建到ProductListQuery这个Query对象上去


(2) 【orderBy】这一个条件处理:在构建PageHelper的时候,应用这个排序条件;需要注意:这儿我们需要【在程序中,规定好有哪些字段可以排序】,这非常重要,有关程序的安全;

一种错误写法:前端传什么条件,我们就原样不动的把其传到MySQL上去进行排序;;;其实,有哪些字段可以排序,我们必须要提前设计好;


(3)以前面构建的Query对象作为条件,去调用我们在Dao层编写查询方法:selectList()方法;;这个方法,在下一部分介绍;


(4)把查询结果,包装成PageInfo对象,返回给调用方;

2.2.在ProductService接口中,反向生成方法的声明;

3.1在ProductMapper中,声明根据条件,查询商品的方法:selectList()方法;

说明:

(1) 可以看到,虽然这儿我们传给Dao层的参数是一个实体类,但我们这儿依旧使用了@Param注解;

3.2在ProductMapper.xml中,编写对应的实现SQL;

  <select id="selectList" parameterType="com.imooc.mall.model.query.ProductListQuery" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List"/>
    from imooc_mall_product
    <where>
      <if test="query.keyword != null">
         and name like #{query.keyword}
      </if>
      <if test="query.categoryIds != null">
        and category_id in
        <foreach collection="query.categoryIds" close=")" item="item" open="(" separator=",">
          #{item}
        </foreach>
      </if>
      and status = 1
    </where>
    order by update_time desc
  </select>

说明:

(1) 入参需要是我们的Query查询类:parameterType=“com.imooc.mall.model.query.ProductListQuery”;

(2) 因为,这儿ProductListQuery中的两个条件是可有可无的;即,如果前端传了条件,这儿就有,如果前端没传条件,这儿就没有;所以,这儿查询SQL的时候,需要判断一下;于是就涉及到了Mybatis的动态SQL了;

● 有关mybatis动态SQL,如有需要可以参考:【动态sql】;

(3) SQL语句,整体分析;

(4) 这儿使用到了MySQL的like模糊查询;

● 有关MySQL的like模糊查询,如有需要可以参考:【WHERE子句条件查询】;

(5) 这儿使用到了MySQL的in子句,用来进行范围判断;

● 有关MySQL的in子句,如有需要可以参考:【WHERE子句条件查询】;

(6) 这儿使用到了Mybatis的foreach子标签;

● 上一次我们使用到Mybatis的foreach子标签是在【SpringBoot电商项目商品模块批量上下架商品接口

● Mybatisforeach子标签:最初,是在【MyBatis批处理】中进行介绍的;如有需要,可以去参考;

至此,前台的【商品列表】接口就开发完成了;


三:测试;

启动项目;

测试1:


测试2:


测试3:


测试4:


测试5:一个在目前的项目中,实际上不存在的情况;