[springboot 開發單體web shop] 8. 商品詳情&評價展示
上文回顧
上節 我們實現了根據搜尋關鍵詞查詢商品列表和根據商品分類查詢,並且使用到了mybatis-pagehelper
外掛,講解了如何使用外掛來幫助我們快速實現分頁資料查詢。本文我們將繼續開發商品詳情頁面和商品留言功能的開發。
需求分析
關於商品詳情頁,和往常一樣,我們先來看一看jd
的示例:
從上面2張圖,我們可以看出來,大體上需要展示給使用者的資訊。比如:商品圖片,名稱,價格,等等。在第二張圖中,我們還可以看到有一個商品評價頁籤
,這些都是我們本節要實現的內容。
商品詳情
開發梳理
我們根據上圖(權當是需求文件,很多需求文件寫的比這個可能還差勁很多...)分析一下,我們的開發大致都要關注哪些points
- 商品標題
- 商品圖片集合
- 商品價格(原價以及優惠價)
- 配送地址(我們的實現不在此,我們後續直接實現在下單邏輯中)
- 商品規格
- 商品分類
- 商品銷量
- 商品詳情
- 商品引數(生產場地,日期等等)
- ...
根據我們梳理出來的資訊,接下來開始編碼就會很簡單了,大家可以根據之前課程講解的,先自行實現一波,請開始你們的表演~
編碼實現
DTO實現
因為我們在實際的資料傳輸過程中,不可能直接把我們的資料庫entity
之間暴露到前端,而且我們商品相關的資料是儲存在不同的資料表中,我們必須要封裝一個ResponseDTO
來對資料進行傳遞。
ProductDetailResponseDTO
包含了商品主表資訊,以及圖片列表、商品規格(不同SKU)以及商品具體引數(產地,生產日期等資訊)
@Data
@ToString
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ProductDetailResponseDTO {
private Products products;
private List<ProductsImg> productsImgList;
private List<ProductsSpec> productsSpecList;
private ProductsParam productsParam;
}
Custom Mapper實現
根據我們之前表的設計,這裡使用生成的通用mapper就可以滿足我們的需求。
Service實現
從我們封裝的要傳遞到前端的ProductDetailResponseDTO
就可以看出,我們可以根據商品id
分別查詢出商品的相關資訊,在controller
進行資料封裝就可以了,來實現我們的查詢介面。
查詢商品主表資訊(名稱,內容等)
在
com.liferunner.service.IProductService
中新增介面方法:/** * 根據商品id查詢商品 * * @param pid 商品id * @return 商品主資訊 */ Products findProductByPid(String pid);
接著,在
com.liferunner.service.impl.ProductServiceImpl
中新增實現方法:@Override @Transactional(propagation = Propagation.SUPPORTS) public Products findProductByPid(String pid) { return this.productsMapper.selectByPrimaryKey(pid); }
直接使用通用mapper根據主鍵查詢就可以了。
同上,我們依次來實現圖片、規格、以及商品引數相關的編碼工作
查詢商品圖片資訊列表
/** * 根據商品id查詢商品規格 * * @param pid 商品id * @return 規格list */ List<ProductsSpec> getProductSpecsByPid(String pid); ---------------------------------------------------------------- @Override public List<ProductsSpec> getProductSpecsByPid(String pid) { Example example = new Example(ProductsSpec.class); val condition = example.createCriteria(); condition.andEqualTo("productId", pid); return this.productsSpecMapper.selectByExample(example); }
查詢商品規格列表
/** * 根據商品id查詢商品規格 * * @param pid 商品id * @return 規格list */ List<ProductsSpec> getProductSpecsByPid(String pid); ------------------------------------------------------------------ @Override public List<ProductsSpec> getProductSpecsByPid(String pid) { Example example = new Example(ProductsSpec.class); val condition = example.createCriteria(); condition.andEqualTo("productId", pid); return this.productsSpecMapper.selectByExample(example); }
查詢商品引數資訊
/** * 根據商品id查詢商品引數 * * @param pid 商品id * @return 引數 */ ProductsParam findProductParamByPid(String pid); ------------------------------------------------------------------ @Override public ProductsParam findProductParamByPid(String pid) { Example example = new Example(ProductsParam.class); val condition = example.createCriteria(); condition.andEqualTo("productId", pid); return this.productsParamMapper.selectOneByExample(example); }
Controller實現
在上面將我們需要的資訊查詢實現之後,然後我們需要在controller對資料進行包裝,之後再返回到前端,供使用者來進行檢視,在com.liferunner.api.controller.ProductController
中新增對外介面/detail/{pid}
,實現如下:
@GetMapping("/detail/{pid}")
@ApiOperation(value = "根據商品id查詢詳情", notes = "根據商品id查詢詳情")
public JsonResponse findProductDetailByPid(
@ApiParam(name = "pid", value = "商品id", required = true)
@PathVariable String pid) {
if (StringUtils.isBlank(pid)) {
return JsonResponse.errorMsg("商品id不能為空!");
}
val product = this.productService.findProductByPid(pid);
val productImgList = this.productService.getProductImgsByPid(pid);
val productSpecList = this.productService.getProductSpecsByPid(pid);
val productParam = this.productService.findProductParamByPid(pid);
val productDetailResponseDTO = ProductDetailResponseDTO
.builder()
.products(product)
.productsImgList(productImgList)
.productsSpecList(productSpecList)
.productsParam(productParam)
.build();
log.info("============查詢到商品詳情:{}==============", productDetailResponseDTO);
return JsonResponse.ok(productDetailResponseDTO);
}
從上述程式碼中可以看到,我們分別查詢了商品、圖片、規格以及引數資訊,使用ProductDetailResponseDTO.builder().build()
封裝成返回到前端的物件。
Test API
按照慣例,寫完程式碼我們需要進行測試。
{
"status": 200,
"message": "OK",
"data": {
"products": {
"id": "smoke-100021",
"productName": "(奔跑的人生) - 中華",
"catId": 37,
"rootCatId": 1,
"sellCounts": 1003,
"onOffStatus": 1,
"createdTime": "2019-09-09T06:45:34.000+0000",
"updatedTime": "2019-09-09T06:45:38.000+0000",
"content": "吸菸有害健康“
},
"productsImgList": [
{
"id": "1",
"productId": "smoke-100021",
"url": "http://www.life-runner.com/product/smoke/img1.png",
"sort": 0,
"isMain": 1,
"createdTime": "2019-07-01T06:46:55.000+0000",
"updatedTime": "2019-07-01T06:47:02.000+0000"
},
{
"id": "2",
"productId": "smoke-100021",
"url": "http://www.life-runner.com/product/smoke/img2.png",
"sort": 1,
"isMain": 0,
"createdTime": "2019-07-01T06:46:55.000+0000",
"updatedTime": "2019-07-01T06:47:02.000+0000"
},
{
"id": "3",
"productId": "smoke-100021",
"url": "http://www.life-runner.com/product/smoke/img3.png",
"sort": 2,
"isMain": 0,
"createdTime": "2019-07-01T06:46:55.000+0000",
"updatedTime": "2019-07-01T06:47:02.000+0000"
}
],
"productsSpecList": [
{
"id": "1",
"productId": "smoke-100021",
"name": "中華",
"stock": 2276,
"discounts": 1.00,
"priceDiscount": 7000,
"priceNormal": 7000,
"createdTime": "2019-07-01T06:54:20.000+0000",
"updatedTime": "2019-07-01T06:54:28.000+0000"
},
],
"productsParam": {
"id": "1",
"productId": "smoke-100021",
"producPlace": "中國",
"footPeriod": "760天",
"brand": "中華",
"factoryName": "中華",
"factoryAddress": "陝西",
"packagingMethod": "盒裝",
"weight": "100g",
"storageMethod": "常溫",
"eatMethod": "",
"createdTime": "2019-05-01T09:38:30.000+0000",
"updatedTime": "2019-05-01T09:38:34.000+0000"
}
},
"ok": true
}
商品評價
在文章一開始我們就看過jd
詳情頁面,有一個詳情頁籤,我們來看一下:
它這個實現比較複雜,我們只實現相對重要的幾個就可以了。
開發梳理
針對上圖中紅色方框圈住的內容,分別有:
- 評價總數
- 好評度(根據好評總數,中評總數,差評總數計算得出)
- 評價等級
- 以及使用者資訊加密展示
- 評價內容
- ...
我們來實現上述分析的相對必要的一些內容。
編碼實現
查詢評價
根據我們需要的資訊,我們需要從使用者表、商品表以及評價表中來聯合查詢資料,很明顯單表通用mapper無法實現,因此我們先來實現自定義查詢mapper,當然資料的傳輸物件是我們需要先來定義的。
Response DTO實現
建立com.liferunner.dto.ProductCommentDTO
.
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ProductCommentDTO {
//評價等級
private Integer commentLevel;
//規格名稱
private String specName;
//評價內容
private String content;
//評價時間
private Date createdTime;
//使用者頭像
private String userFace;
//使用者暱稱
private String nickname;
}
Custom Mapper實現
在com.liferunner.custom.ProductCustomMapper
中新增查詢介面方法:
/***
* 根據商品id 和 評價等級查詢評價資訊
* <code>
* Map<String, Object> paramMap = new HashMap<>();
* paramMap.put("productId", pid);
* paramMap.put("commentLevel", level);
*</code>
* @param paramMap
* @return java.util.List<com.liferunner.dto.ProductCommentDTO>
* @throws
*/
List<ProductCommentDTO> getProductCommentList(@Param("paramMap") Map<String, Object> paramMap);
在mapper/custom/ProductCustomMapper.xml
中實現該介面方法的SQL:
<select id="getProductCommentList" resultType="com.liferunner.dto.ProductCommentDTO" parameterType="Map">
SELECT
pc.comment_level as commentLevel,
pc.spec_name as specName,
pc.content as content,
pc.created_time as createdTime,
u.face as userFace,
u.nickname as nickname
FROM items_comments pc
LEFT JOIN users u
ON pc.user_id = u.id
WHERE pc.item_id = #{paramMap.productId}
<if test="paramMap.commentLevel != null and paramMap.commentLevel != ''">
AND pc.comment_level = #{paramMap.commentLevel}
</if>
</select>
如果沒有傳遞評價級別的話,預設查詢全部評價資訊。
Service 實現
在com.liferunner.service.IProductService
中新增查詢介面方法:
/**
* 查詢商品評價
*
* @param pid 商品id
* @param level 評價級別
* @param pageNumber 當前頁碼
* @param pageSize 每頁展示多少條資料
* @return 通用分頁結果檢視
*/
CommonPagedResult getProductComments(String pid, Integer level, Integer pageNumber, Integer pageSize);
在com.liferunner.service.impl.ProductServiceImpl
實現該方法:
@Override
public CommonPagedResult getProductComments(String pid, Integer level, Integer pageNumber, Integer pageSize) {
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("productId", pid);
paramMap.put("commentLevel", level);
// mybatis-pagehelper
PageHelper.startPage(pageNumber, pageSize);
val productCommentList = this.productCustomMapper.getProductCommentList(paramMap);
for (ProductCommentDTO item : productCommentList) {
item.setNickname(SecurityTools.HiddenPartString4SecurityDisplay(item.getNickname()));
}
// 獲取mybatis外掛中獲取到資訊
PageInfo<?> pageInfo = new PageInfo<>(productCommentList);
// 封裝為返回到前端分頁元件可識別的檢視
val commonPagedResult = CommonPagedResult.builder()
.pageNumber(pageNumber)
.rows(productCommentList)
.totalPage(pageInfo.getPages())
.records(pageInfo.getTotal())
.build();
return commonPagedResult;
}
因為評價過多會使用到分頁,這裡使用通用分頁返回結果,關於分頁,可檢視學習分頁傳送門。
Controller實現
在com.liferunner.api.controller.ProductController
中新增對外查詢介面:
@GetMapping("/comments")
@ApiOperation(value = "查詢商品評價", notes = "根據商品id查詢商品評價")
public JsonResponse getProductComment(
@ApiParam(name = "pid", value = "商品id", required = true)
@RequestParam String pid,
@ApiParam(name = "level", value = "評價級別", required = false, example = "0")
@RequestParam Integer level,
@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(pid)) {
return JsonResponse.errorMsg("商品id不能為空!");
}
if (null == pageNumber || 0 == pageNumber) {
pageNumber = DEFAULT_PAGE_NUMBER;
}
if (null == pageSize || 0 == pageSize) {
pageSize = DEFAULT_PAGE_SIZE;
}
log.info("============查詢商品評價:{}==============", pid);
val productComments = this.productService.getProductComments(pid, level, pageNumber, pageSize);
return JsonResponse.ok(productComments);
}
FBI WARNING:
@ApiParam(name = "level", value = "評價級別", required = false, example = "0")
@RequestParam Integer level
關於ApiParam引數,如果接收引數為非字串型別,一定要定義example為對應型別的示例值,否則Swagger在訪問過程中會報example轉換錯誤,因為example預設為""空字串,會轉換失敗。例如我們刪除掉level
這個欄位中的example=”0“,如下為錯誤資訊(但是並不影響程式使用。)
2019-11-23 15:51:45 WARN AbstractSerializableParameter:421 - Illegal DefaultValue null for parameter type integer
java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.valueOf(Long.java:803)
at io.swagger.models.parameters.AbstractSerializableParameter.getExample(AbstractSerializableParameter.java:412)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:688)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:721)
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:166)
at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:119)
Test API
福利講解
新增Propagation.SUPPORTS和不加的區別
有心的小夥伴肯定又注意到了,在Service中處理查詢時,我一部分使用了@Transactional(propagation = Propagation.SUPPORTS)
,一部分查詢又沒有新增事務,那麼這兩種方式有什麼不一樣呢?接下來,我們來揭開神祕的面紗。
Propagation.SUPPORTS
/** * Support a current transaction, execute non-transactionally if none exists. * Analogous to EJB transaction attribute of the same name. * <p>Note: For transaction managers with transaction synchronization, * {@code SUPPORTS} is slightly different from no transaction at all, * as it defines a transaction scope that synchronization will apply for. * As a consequence, the same resources (JDBC Connection, Hibernate Session, etc) * will be shared for the entire specified scope. Note that this depends on * the actual synchronization configuration of the transaction manager. * @see org.springframework.transaction.support.AbstractPlatformTransactionManager#setTransactionSynchronization */ SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),
主要關注
Support a current transaction, execute non-transactionally if none exists.
從字面意思來看,就是如果當前環境有事務,我就加入到當前事務;如果沒有事務,我就以非事務的方式執行。從這方面來看,貌似我們加不加這一行其實都沒啥差別。劃重點:NOTE,對於一個帶有事務同步的管理器來說,這裡有一丟丟的小區別啦。(所以大家在讀註釋的時候,一定要看這個Note.往往這裡面會有好東西給我們,就相當於我們的大喇叭!)
這個同步事務管理器定義了一個事務同步的一個範圍,如果加了這個註解,那麼就等同於我讓你來管我啦,你裡面的資源我想用就可以用(JDBC Connection, Hibernate Session).
結論1
SUPPORTS 標註的方法可以獲取和當前事務環境一致的 Connection 或 Session,不使用的話一定是一個新的連線;
再注意下面又一個NOTE,即便上面的配置加入了,但是事務管理器的實際同步配置
會影響到真實的執行到底是否會用你。看它的說明:@see org.springframework.transaction.support.AbstractPlatformTransactionManager#setTransactionSynchronization
.
/**
* Set when this transaction manager should activate the thread-bound
* transaction synchronization support. Default is "always".
* <p>Note that transaction synchronization isn't supported for
* multiple concurrent transactions by different transaction managers.
* Only one transaction manager is allowed to activate it at any time.
* @see #SYNCHRONIZATION_ALWAYS
* @see #SYNCHRONIZATION_ON_ACTUAL_TRANSACTION
* @see #SYNCHRONIZATION_NEVER
* @see TransactionSynchronizationManager
* @see TransactionSynchronization
*/
public final void setTransactionSynchronization(int transactionSynchronization) {
this.transactionSynchronization = transactionSynchronization;
}
描述資訊只是說在同一個事務管理器才能起作用,並沒有什麼實際意義,我們來看一下TransactionSynchronization
具體的內容:
package org.springframework.transaction.support;
import java.io.Flushable;
public interface TransactionSynchronization extends Flushable {
/** Completion status in case of proper commit. */
int STATUS_COMMITTED = 0;
/** Completion status in case of proper rollback. */
int STATUS_ROLLED_BACK = 1;
/** Completion status in case of heuristic mixed completion or system errors. */
int STATUS_UNKNOWN = 2;
/**
* Suspend this synchronization.
* Supposed to unbind resources from TransactionSynchronizationManager if managing any.
* @see TransactionSynchronizationManager#unbindResource
*/
default void suspend() {
}
/**
* Resume this synchronization.
* Supposed to rebind resources to TransactionSynchronizationManager if managing any.
* @see TransactionSynchronizationManager#bindResource
*/
default void resume() {
}
/**
* Flush the underlying session to the datastore, if applicable:
* for example, a Hibernate/JPA session.
* @see org.springframework.transaction.TransactionStatus#flush()
*/
@Override
default void flush() {
}
/**
* ...
*/
default void beforeCommit(boolean readOnly) {
}
/**
* ...
*/
default void beforeCompletion() {
}
/**
* ...
*/
default void afterCommit() {
}
/**
* ...
*/
default void afterCompletion(int status) {
}
}
事務管理器可以通過org.springframework.transaction.support.AbstractPlatformTransactionManager#setTransactionSynchronization(int)
來對當前事務進行行為干預,比如將它設定為1,可以執行事務回撥,設定為2,表示出錯了,但是如果沒有加入PROPAGATION.SUPPORTS
註解的話,即便你在當前事務中,你也不能對我進行操作和變更。
結論2
新增
PROPAGATION.SUPPORTS
之後,當前查詢中可以對當前的事務進行設定回撥動作,不新增就不行。
原始碼下載
Github 傳送門
Gitee 傳送門
下節預告
下一節我們將繼續開發商品詳情展示以及商品評價業務,在過程中使用到的任何開發元件,我都會通過專門的一節來進行介紹的,兄弟們末慌!
gogogo