上節 咱們實現了仿jd
的輪播廣告以及商品分類的功能,而且講解了不一樣的注入方式,本節咱們將繼續實現咱們的電商主業務,商品信息的展現。前端
首先,在咱們開始本節編碼以前,咱們先來分析一下都有哪些地方會對商品進行展現,打開jd
首頁,鼠標下拉能夠看到以下:
java
能夠看到,在大類型下查詢了部分商品在首頁進行展現(能夠是最新的,也能夠是網站推薦等等),而後點擊任何一個分類,能夠看到以下:
mysql
咱們通常進到電商網站以後,最經常使用的一個功能就是搜索,搜索鋼琴) 結果以下:
git
選擇任意一個商品點擊,均可以進入到詳情頁面,這個是單個商品的信息展現。
綜上,咱們能夠知道,要實現一個電商平臺的商品展現,最基本的包含:github
接下來,咱們就能夠開始商品相關的業務開發了。spring
咱們首先來實如今首頁展現的推薦商品列表,來看一下都須要展現哪些信息,以及如何進行展現。sql
遵循開發順序,自下而上,若是基礎mapper解決不了,那麼優先編寫SQL mapper,由於咱們須要在同一張表中根據parent_id
遞歸的實現數據查詢,固然咱們這裏使用的是表連接
的方式實現。所以,common mapper
沒法知足咱們的需求,須要自定義mapper實現。shell
和上節根據一級分類查詢子分類同樣,在項目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
project 建立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; } }
接着,在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); } ... }
編寫完成以後,咱們須要對咱們的代碼進行測試驗證,仍是經過使用RestService
插件來實現,固然,你們也能夠經過Postman來測試,結果以下:
如開文之初咱們看到的京東商品列表同樣,咱們先分析一下在商品列表頁面都須要哪些元素信息?
商品列表的展現按照咱們以前的分析,總共分爲2大類:
在這兩類中展現的商品列表數據,除了數據來源不一樣之外,其餘元素基本都保持一致,那麼咱們是否可使用統一的接口來根據參數實現隔離呢? 理論上不存在問題,徹底能夠經過傳參判斷的方式進行數據回傳,可是,在咱們實現一些可預見的功能需求時,必定要給本身的開發預留後路,也就是咱們常說的可拓展性
,基於此,咱們會分開實現各自的接口,以便於後期的擴展。
接着來分析在列表頁中咱們須要展現的元素,首先由於須要分上述兩種狀況,所以咱們須要在咱們API設計的時候分別處理,針對於
1.分類的商品列表展現,須要傳入的參數有:
分頁相關(由於咱們不可能把數據庫中全部的商品都取出來)
2.關鍵詞查詢商品列表,須要傳入的參數有:
分頁相關(由於咱們不可能把數據庫中全部的商品都取出來)
須要在頁面展現的信息有:
根據上面咱們的分析,接下來開始咱們的編碼:
根據咱們的分析,確定不會在一張表中把全部數據獲取全,所以咱們須要進行多表聯查,故咱們須要在自定義mapper中實現咱們的功能查詢.
根據咱們前面分析的前端須要展現的信息,咱們來定義一個用於展現這些信息的對象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; //商品優惠,咱們直接計算以後返回優惠後價格 }
在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)
的關係。
它的選擇是按照順序自上而下,一旦有任何一個知足條件,則選擇退出。
而後在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
插件,會在下面的福利講解中分解。
繼續在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; }
測試的參數分別是:categoryId : 51 ,sortby : price,pageNumber : 1,pageSize : 5
能夠看到,咱們查詢到7條數據,總頁數totalPage
爲2,而且根據價格從小到大進行了排序,證實咱們的編碼是正確的。接下來,經過相同的代碼邏輯,咱們繼續實現根據搜索關鍵詞進行查詢。
使用上面實現的com.liferunner.dto.SearchProductDTO
.
在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>
在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)
惟一的區別就是它是確定搜索關鍵詞來進行數據查詢,使用重載的目的是爲了咱們後續不一樣類型的業務擴展而考慮的。
在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); }
測試參數: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>
答案就在這裏:依賴傳送門
咱們使用的是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
呢?這是由於咱們實際開發過程當中,爲了數據結構的一致性作的一次結構封裝,你也可不實現該步驟,都是對結果沒有任何影響的。
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
參數,在代碼中,咱們很明確的知道,咱們並無顯示的去搜索總數和查詢條數,能夠肯定它就是插件幫咱們實現的。
下一節咱們將繼續開發商品詳情展現以及商品評價業務,在過程當中使用到的任何開發組件,我都會經過專門的一節來進行介紹的,兄弟們末慌!
gogogo!