[springboot 開發(fā)單體web shop] 6. 商品分類和輪播廣告展示

商品分類&輪播廣告


因最近又被困在了OSGI技術(shù)POC,更新進(jìn)度有點慢,希望大家不要怪罪哦。

上節(jié) 我們實現(xiàn)了登錄之后前端的展示,如:

登錄展示效果

子分類

接著,我們來實現(xiàn)左側(cè)分類欄目的功能。

商品分類|ProductCategory


從上圖我們可以看出,商品的分類其實是有層級關(guān)系的,而且這種關(guān)系一般都是無限層級。在我們的實現(xiàn)中,為了效果的展示,我們僅僅是展示3級分類,在大多數(shù)的中小型電商系統(tǒng)中,三級分類完全足夠應(yīng)對SKU的分類。

需求分析


先來分析分類都包含哪些元素,以jd為例:

  • logo(logo) 有的分類文字前面會有小標(biāo)
  • 分類展示主圖(img_url)
  • 主標(biāo)題(title)
  • 副標(biāo)題/Slogan
  • 圖片跳轉(zhuǎn)地址(img_link_url)-- 大多數(shù)時候我們點擊分類都會分類Id跳轉(zhuǎn)到固定的分類商品列表展示頁面,但是在一些特殊的場景,比如我們要做一個活動,希望可以點擊某一個分類的主圖直接定位到活動頁面,這個url就可以使用了。
  • 上級分類(parent_id)
  • 背景色(bg_color)
  • 順序(sort)
  • 當(dāng)前分類級別(type)

開發(fā)梳理


在上一小節(jié),我們簡單分析了一下要實現(xiàn)商品分類的一些points,那么我們最好在每次拿到需求【開發(fā)之前】,對需求進(jìn)行拆解,然后分解開發(fā)流程,這樣可以保證我們更好的理解需求,以及在開發(fā)之前發(fā)現(xiàn)一部分不合理的需求,并且如果需求設(shè)計不合理的話,開發(fā)人員完全有權(quán),也有責(zé)任告知PM。大家的終極目的都是為了我們做的產(chǎn)品更加合理,好用,受歡迎!

  • 首次展示,僅僅讀取一級分類(Root)
  • 根據(jù)一級分類查詢二三級子分類

編碼實現(xiàn)


查詢一級分類

Service實現(xiàn)

1.在com.liferunner.service中創(chuàng)建service 接口ICategoryService.java, 編寫查詢所有一級分類的方法getAllRootCategorys,如下:

package com.liferunner.service;
import com.liferunner.dto.CategoryResponseDTO;
import com.liferunner.dto.SecondSubCategoryResponseDTO;
import java.util.List;
/**
 * ICategoryService for : 分類service
 *
 * @author <a href="mailto:magicianisaac@gmail.com">Isaac.Zhang | 若初</a>
 * @since 2019/11/13
 */
public interface ICategoryService {
    /**
     * 獲取所有有效的一級分類(根節(jié)點)
     *
     * @return
     */
    List<CategoryResponseDTO> getAllRootCategorys();
}

2.編寫實現(xiàn)類com.liferunner.service.ICategoryService.java

@Service
@Slf4j
public class CategorySericeImpl implements ICategoryService {
    @Autowired
    private CategoryMapper categoryMapper;
    
    @Override
    public List<CategoryResponseDTO> getAllRootCategorys() {
        Example example = new Example(Category.class);
        val conditions = example.createCriteria();
        conditions.andEqualTo("type", CategoryTypeEnum.ROOT.type);
        val categoryList = this.categoryMapper.selectByExample(example);
        //聲明返回對象
        List<CategoryResponseDTO> categoryResponseDTOS = new ArrayList<>();
        if (!CollectionUtils.isEmpty(categoryList)) {
            //賦值
            CategoryResponseDTO dto;
            for (Category category : categoryList) {
                dto = new CategoryResponseDTO();
                BeanUtils.copyProperties(category, dto);
                categoryResponseDTOS.add(dto);
            }
        }
        return categoryResponseDTOS;
    }
}

上述代碼很好理解,創(chuàng)建tk.mybatis.mapper.entity.Example,將條件傳入,然后使用通用Mapper查詢到type=1的一級分類,接著將查到的對象列表轉(zhuǎn)換為DTO對象列表。

Controller實現(xiàn)

一般情況下,此類查詢都會出現(xiàn)在網(wǎng)站的首頁,因此我們來創(chuàng)建一個com.liferunner.api.controller.IndexController,并對外暴露一個查詢一級分類的接口:

package com.liferunner.api.controller;

import com.liferunner.service.ICategoryService;
import com.liferunner.service.IProductService;
import com.liferunner.service.ISlideAdService;
import com.liferunner.utils.JsonResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import java.util.Collections;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
 * IndexController for : 首頁controller
 *
 * @author <a href="mailto:magicianisaac@gmail.com">Isaac.Zhang | 若初</a>
 * @since 2019/11/12
 */
@RestController
@RequestMapping("/index")
@Api(value = "首頁信息controller", tags = "首頁信息接口API")
@Slf4j
public class IndexController {
    @Autowired
    private ICategoryService categoryService;

    @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

編寫完成之后,我們需要對我們的代碼進(jìn)行測試驗證,還是通過使用RestService插件來實現(xiàn),當(dāng)然,大家也可以通過Postman來測試。

{
  "status": 200,
  "message": "OK",
  "data": [
    {
      "id": 1,
      "name": "煙酒",
      "type": 1,
      "parentId": 0,
      "logo": "img/cake.png",
      "slogan": "吸煙受害健康",
      "catImage": "http://www.life-runner.com/shop/category/cake.png",
      "bgColor": "#fe7a65"
    },
    {
      "id": 2,
      "name": "服裝",
      "type": 1,
      "parentId": 0,
      "logo": "img/cookies.png",
      "slogan": "我選擇我喜歡",
      "catImage": "http://www.life-runner.com/shop/category/cookies.png",
      "bgColor": "#f59cec"
    },
    {
      "id": 3,
      "name": "鞋帽",
      "type": 1,
      "parentId": 0,
      "logo": "img/meat.png",
      "slogan": "飛一般的感覺",
      "catImage": "http://www.life-runner.com/shop/category/meat.png",
      "bgColor": "#b474fe"
    }
  ],
  "ok": true
}

根據(jù)一級分類查詢子分類

因為根據(jù)一級id查詢子分類的時候,我們是在同一張表中做自連接查詢,因此,通用mapper已經(jīng)不適合我們的使用,因此我們需要自定義mapper來實現(xiàn)我們的需求。

自定義Mybatis Mapper實現(xiàn)

在之前的編碼中,我們都是使用的插件幫我們實現(xiàn)的通用Mapper,但是這種查詢只能處理簡單的單表CRUD,一旦我們需要SQL 包含一部分邏輯處理的時候,那就必須得自己來編寫了,let's code.
1.在項目mscx-shop-mapper中,創(chuàng)建一個新的custom package,在該目錄下創(chuàng)建自定義mappercom.liferunner.custom.CategoryCustomMapper。

public interface CategoryCustomMapper {
    List<SecondSubCategoryResponseDTO> getSubCategorys(Integer parentId);
}

2.resources目錄下創(chuàng)建目錄mapper.custom,以及創(chuàng)建和上面的接口相同名稱的XML文件mapper/custom/CategoryCustomMapper.xml

<?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.CategoryCustomMapper">
    <resultMap id="subCategoryDTO" type="com.liferunner.dto.SecondSubCategoryResponseDTO">
        <id column="id" jdbcType="INTEGER" property="id"/>
        <result column="name" jdbcType="VARCHAR" property="name"/>
        <result column="type" jdbcType="INTEGER" property="type"/>
        <result column="parentId" jdbcType="INTEGER" property="parentId"/>
        <collection property="thirdSubCategoryResponseDTOList" ofType="com.liferunner.dto.ThirdSubCategoryResponseDTO">
            <id column="subId" jdbcType="INTEGER" property="subId"/>
            <result column="subName" jdbcType="VARCHAR" property="subName"/>
            <result column="subType" jdbcType="INTEGER" property="subType"/>
            <result column="subParentId" jdbcType="INTEGER" property="subParentId"/>
        </collection>
    </resultMap>
    <select id="getSubCategorys" resultMap="subCategoryDTO" parameterType="INTEGER">
        SELECT p.id as id,p.`name` as `name`,p.`type` as `type`,p.father_id as parentId,
        c.id as subId,c.`name` as subName,c.`type` as subType,c.parent_id as subParentId
        FROM category p
        LEFT JOIN category c
        ON p.id = c.parent_id
        WHERE p.parent_id = ${parentId};
    </select>
</mapper>

TIPS
上述創(chuàng)建的package,一定要在項目的啟動類com.liferunner.api.ApiApplication中修改@MapperScan(basePackages = { "com.liferunner.mapper", "com.liferunner.custom"}),如果不把我們的custom package加上,會造成掃描不到而報錯。

在上面的xml中,我們定義了兩個DTO對象,分別用來處理二級和三級分類的DTO,實現(xiàn)如下:

@Data
@ToString
public class SecondSubCategoryResponseDTO {
    /**
     * 主鍵
     */
    private Integer id;

    /**
     * 分類名稱
     */
    private String name;

    /**
     * 分類類型
     1:一級大分類
     2:二級分類
     3:三級小分類
     */
    private Integer type;

    /**
     * 父id
     */
    private Integer parentId;

    List<ThirdSubCategoryResponseDTO> thirdSubCategoryResponseDTOList;
}
---
    
@Data
@ToString
public class ThirdSubCategoryResponseDTO {
    /**
     * 主鍵
     */
    private Integer subId;

    /**
     * 分類名稱
     */
    private String subName;

    /**
     * 分類類型
     1:一級大分類
     2:二級分類
     3:三級小分類
     */
    private Integer subType;

    /**
     * 父id
     */
    private Integer subParentId;
}
Service實現(xiàn)

編寫完自定義mapper之后,我們就可以繼續(xù)編寫service了,在com.liferunner.service.ICategoryService中新增一個方法:getAllSubCategorys(parentId).如下:

public interface ICategoryService {
    ...
    /**
     * 根據(jù)一級分類獲取子分類
     *
     * @param parentId 一級分類id
     * @return 子分類list
     */
    List<SecondSubCategoryResponseDTO> getAllSubCategorys(Integer parentId);
}

com.liferunner.service.impl.CategorySericeImpl實現(xiàn)上述方法:

@Service
@Slf4j
public class CategorySericeImpl implements ICategoryService {
    @Autowired
    private CategoryMapper categoryMapper;

    @Autowired
    private CategoryCustomMapper categoryCustomMapper;
    ...
    @Override
    @Transactional(propagation = Propagation.SUPPORTS)
    public List<SecondSubCategoryResponseDTO> getAllSubCategorys(Integer parentId) {
        return this.categoryCustomMapper.getSubCategorys(parentId);
    }
}
Controller實現(xiàn)
@RestController
@RequestMapping("/index")
@Api(value = "首頁信息controller", tags = "首頁信息接口API")
@Slf4j
public class IndexController {
    @Autowired
    private ICategoryService categoryService;
    ...
    @GetMapping("/subCategorys/{parentId}")
    @ApiOperation(value = "查詢子分類", notes = "根據(jù)一級分類id查詢子分類")
    public JsonResponse findAllSubCategorys(
            @ApiParam(name = "parentId", value = "一級分類id", required = true)
            @PathVariable Integer parentId) {
        log.info("============查詢id = {}的子分類==============", parentId);
        val categoryResponseDTOS = this.categoryService.getAllSubCategorys(parentId);
        if (CollectionUtils.isEmpty(categoryResponseDTOS)) {
            log.info("============未查詢到任何分類==============");
            return JsonResponse.ok(Collections.EMPTY_LIST);
        }
        log.info("============子分類查詢result:{}==============", categoryResponseDTOS);
        return JsonResponse.ok(categoryResponseDTOS);
    }
}
Test API
{
  "status": 200,
  "message": "OK",
  "data": [
    {
      "id": 11,
      "name": "國產(chǎn)",
      "type": 2,
      "parentId": 1,
      "thirdSubCategoryResponseDTOList": [
        {
          "subId": 37,
          "subName": "中華",
          "subType": 3,
          "subParentId": 11
        },
        {
          "subId": 38,
          "subName": "冬蟲夏草",
          "subType": 3,
          "subParentId": 11
        },
        {
          "subId": 39,
          "subName": "南京",
          "subType": 3,
          "subParentId": 11
        },
        {
          "subId": 40,
          "subName": "云煙",
          "subType": 3,
          "subParentId": 11
        }
      ]
    },
    {
      "id": 12,
      "name": "外煙",
      "type": 2,
      "parentId": 1,
      "thirdSubCategoryResponseDTOList": [
        {
          "subId": 44,
          "subName": "XXXXX",
          "subType": 3,
          "subParentId": 12
        },
        {
          "subId": 45,
          "subName": "RRRRR",
          "subType": 3,
          "subParentId": 12
        }
      ]
    }
  ],
  "ok": true
}

以上我們就已經(jīng)實現(xiàn)了和jd類似的商品分類的功能實現(xiàn)。

輪播廣告|SlideAD


需求分析

這個就是jd或者tb首先的最頂部的廣告圖片是一樣的,每隔1秒自動切換圖片。接下來我們分析一下輪播圖中都包含哪些信息:

Slide Images

  • 圖片(img_url)是最基本的
  • 圖片跳轉(zhuǎn)連接(img_link_url),這個是在我們點擊這個圖片的時候需要跳轉(zhuǎn)到的頁面
  • 有的可以直接跳轉(zhuǎn)到商品詳情頁面
  • 有的可以直接跳轉(zhuǎn)到某一分類商品列表頁面
  • 輪播圖的播放順序(sort)
  • ...

開發(fā)梳理

直接查詢出所有的有效的輪播圖片,并且進(jìn)行排序。

編碼實現(xiàn)

Service 實現(xiàn)

和商品分類實現(xiàn)一樣,在mscx-shop-service中創(chuàng)建com.liferunner.service.ISlideAdService并實現(xiàn),代碼如下:

public interface ISlideAdService {
    /**
     * 查詢所有可用廣告并排序
     * @param isShow
     * @return
     */
    List<SlideAdResponseDTO> findAll(Integer isShow, String sortRanking);
}
@Service
@Slf4j
public class SlideAdServiceImpl implements ISlideAdService {

    // 注入mapper
    private final SlideAdsMapper slideAdsMapper;

    @Autowired
    public SlideAdServiceImpl(SlideAdsMapper slideAdsMapper) {
        this.slideAdsMapper = slideAdsMapper;
    }

    @Override
    public List<SlideAdResponseDTO> findAll(Integer isShow, String sortRanking) {
        Example example = new Example(SlideAds.class);
        //設(shè)置排序
        if (StringUtils.isBlank(sortRanking)) {
            example.orderBy("sort").asc();
        } else {
            example.orderBy("sort").desc();
        }
        val conditions = example.createCriteria();
        conditions.andEqualTo("isShow", isShow);
        val slideAdsList = this.slideAdsMapper.selectByExample(example);
        //聲明返回對象
        List<SlideAdResponseDTO> slideAdResponseDTOList = new ArrayList<>();
        if (!CollectionUtils.isEmpty(slideAdsList)) {
            //賦值
            SlideAdResponseDTO dto;
            for (SlideAds slideAds : slideAdsList) {
                dto = new SlideAdResponseDTO();
                BeanUtils.copyProperties(slideAds, dto);
                slideAdResponseDTOList.add(dto);
            }
        }
        return slideAdResponseDTOList;
    }
}

從上述可以看到,這里我使用的是構(gòu)造函數(shù)注入SlideAdsMapper,其余代碼單表查詢沒什么特別的,根據(jù)條件查詢輪播圖,并返回結(jié)果,返回的對象是com.liferunner.dto.SlideAdResponseDTO列表,代碼如下:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ApiModel(value = "輪播廣告返回DTO", description = "輪播廣告返回DTO")
public class SlideAdResponseDTO{
    /**
     * 主鍵
     */
    private String id;

    /**
     * 圖片地址
     */
    private String imageUrl;

    /**
     *  背景顏色
     */
    private String backgroundColor;

    /**
     * 商品id
     */
    private String productId;

    /**
     * 商品分類id
     */
    private String catId;

    /**
     * 圖片跳轉(zhuǎn)URL
     */
    private String imageLinkUrl;

    /**
     * 輪播圖類型 用于判斷,可以根據(jù)商品id或者分類進(jìn)行頁面跳轉(zhuǎn),1:商品 2:分類 3:鏈接url
     */
    private Integer type;

    /**
     * 輪播圖展示順序 輪播圖展示順序,從小到大
     */
    private Integer sort;

    /**
     * 是否展示 是否展示,1:展示    0:不展示
     */
    private Integer isShow;

    /**
     * 創(chuàng)建時間 創(chuàng)建時間
     */
    private Date createTime;

    /**
     * 更新時間 更新
     */
    private Date updateTime;
}

Controller實現(xiàn)

com.liferunner.api.controller.IndexController中,新添加一個查詢輪播圖API,代碼如下:

    @Autowired
    private ISlideAdService slideAdService;

    @GetMapping("/slideAds")
    @ApiOperation(value = "查詢輪播廣告", notes = "查詢輪播廣告接口")
    public JsonResponse findAllSlideList() {
        log.info("============查詢所有輪播廣告,isShow={},sortRanking={}=============="
                , 1, "desc");
        val slideAdsList = this.slideAdService.findAll(1, "desc");
        if (CollectionUtils.isEmpty(slideAdsList)) {
            log.info("============未查詢到任何輪播廣告==============");
            return JsonResponse.ok(Collections.EMPTY_LIST);
        }
        log.info("============輪播廣告查詢result:{}=============="
                , slideAdsList);
        return JsonResponse.ok(slideAdsList);
    }

Test API

{
  "status": 200,
  "message": "OK",
  "data": [
    {
      "id": "slide-100002",
      "imageUrl": "http://www.life-runner.com/2019/11/CpoxxF0ZmH6AeuRrAAEZviPhyQ0768.png",
      "backgroundColor": "#55be59",
      "productId": "",
      "catId": "133",
      "type": 2,
      "sort": 2,
      "isShow": 1,
      "createTime": "2019-10-11T21:33:01.000+0000",
      "updateTime": "2019-10-11T21:33:02.000+0000"
    },
    {
      "id": "slide-100003",
      "imageUrl": "http://www.life-runner.com/2019/11/CpoxxF0ZmHuAPlXvAAFe-H5_-Nw961.png",
      "backgroundColor": "#ff9801",
      "productId": "y200008",
      "catId": "",
      "type": 1,
      "sort": 1,
      "isShow": 1,
      "createTime": "2019-10-11T21:33:01.000+0000",
      "updateTime": "2019-10-11T21:33:02.000+0000"
    }
  ],
  "ok": true
}

福利講解

在我們的實現(xiàn)代碼中,有心的同學(xué)可以看到,我使用了3種不同的Bean注入方式:

  • 屬性注入
    @Autowired
    private ISlideAdService slideAdService;
  • 構(gòu)造函數(shù)注入
    // 注入mapper
    private final SlideAdsMapper slideAdsMapper;

    @Autowired
    public SlideAdServiceImpl(SlideAdsMapper slideAdsMapper) {
        this.slideAdsMapper = slideAdsMapper;
    }
  • Lombok插件注入(本質(zhì)也是構(gòu)造器注入,代碼會動態(tài)生成。)
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class ProductServiceImpl implements IProductService {
    // RequiredArgsConstructor 構(gòu)造器注入
    private final ProductCustomMapper productCustomMapper;
    private final ProductsMapper productsMapper;
    ...
}

那么,這幾種注入都有什么區(qū)別呢?首先我們下了解一下Spring的注入是干什么的?

Spring提出了依賴注入的思想,即依賴類不由程序員實例化,而是通過Spring容器幫我們new指定實例并且將實例注入到需要該對象的類中。
依賴注入的另一種說法是"控制反轉(zhuǎn)"。通俗的理解是:平常我們new一個實例,這個實例的控制權(quán)是我們程序員, 而控制反轉(zhuǎn)是指new實例工作不由我們程序員來做而是交給Spring容器來做。

在傳統(tǒng)的SpringMVC中,大家使用的都是XML注入,比如:

<!--配置bean,配置后該類由spring管理--> 
<bean name="CategorySericeImpl" class="com.liferunner.service.impl.CategorySericeImpl"> 
<!--注入配置當(dāng)前類中相應(yīng)的屬性--> 
<property name="categoryMapper" ref="categoryMapper"></property> 
</bean> 
<bean name="categoryMapper" class="com.liferunner.mapper.CategoryMapper"></bean>

注入之后,使用@Autowired,我們可以很方便的自動從IOC容器中查找屬性,并返回。

@Autowired的原理
在啟動spring IoC時,容器自動裝載了一個AutowiredAnnotationBeanPostProcessor后置處理器,當(dāng)容器掃描到@Autowied、@Resource或@Inject時,就會在IoC容器自動查找需要的bean,并裝配給該對象的屬性。
注意事項:
在使用@Autowired時,首先在容器中查詢對應(yīng)類型的bean
如果查詢結(jié)果剛好為一個,就將該bean裝配給@Autowired指定的數(shù)據(jù)
如果查詢的結(jié)果不止一個,那么@Autowired會根據(jù)名稱來查找。
如果查詢的結(jié)果為空,那么會拋出異常。解決方法時,使用required=false
source傳送門

上述三種注解方式,其實本質(zhì)上還是注入的2種:set屬性注入 & 構(gòu)造器注入,使用方式都可以,根據(jù)個人喜好來用,本人喜歡使用lombok插件注入是因為,它將代碼整合在一起,更加符合我們Spring自動注入的規(guī)范。

源碼下載


Github 傳送門
Gitee 傳送門

下節(jié)預(yù)告


下一節(jié)我們將繼續(xù)開發(fā)我們電商的核心部分-商品列表和詳情展示,在過程中使用到的任何開發(fā)組件,我都會通過專門的一節(jié)來進(jìn)行介紹的,兄弟們末慌!

gogogo!

奔跑的人生 | 博客園 | segmentfault | spring4all | csdn | 掘金 | OSChina | 簡書 | 頭條 | 知乎 | 51CTO

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容