[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
之后,当前查询中可以对当前的事务进行设置回调动作,不添加就不行。
源码下载
下节预告
下一节我们将继续开发商品详情展示以及商品评价业务,在过程中使用到的任何开发组件,我都会通过专门的一节来进行介绍的,兄弟们末慌!
gogogo!
[springboot 开发单体web shop] 8. 商品详情&评价展示的更多相关文章
- [springboot 开发单体web shop] 7. 多种形式提供商品列表
上文回顾 上节 我们实现了仿jd的轮播广告以及商品分类的功能,并且讲解了不同的注入方式,本节我们将继续实现我们的电商主业务,商品信息的展示. 需求分析 首先,在我们开始本节编码之前,我们先来分析一下都 ...
- [springboot 开发单体web shop] 1. 前言介绍和环境搭建
前言介绍和环境搭建 简述 springboot 本身是为了做服务化用的,我们为什么要反其道使用它来开发一份单体web应用呢? 在我们现实的开发工作中,还有大量的业务系统使用的是单体应用,特别是对于中小 ...
- [springboot 开发单体web shop] 6. 商品分类和轮播广告展示
商品分类&轮播广告 因最近又被困在了OSGI技术POC,更新进度有点慢,希望大家不要怪罪哦. 上节 我们实现了登录之后前端的展示,如: 接着,我们来实现左侧分类栏目的功能. ## 商品分类|P ...
- [springboot 开发单体web shop] 4. Swagger生成Javadoc
Swagger生成JavaDoc 在日常的工作中,特别是现在前后端分离模式之下,接口的提供造成了我们前后端开发人员的沟通 成本大量提升,因为沟通不到位,不及时而造成的[撕币]事件都成了日常工作.特别是 ...
- [springboot 开发单体web shop] 5. 用户登录及首页展示
用户登录及前端展示 用户登录 在之前的文章中我们实现了用户注册和验证功能,接下来我们继续实现它的登录,以及登录成功之后要在页面上显示的信息. 接下来,我们来编写代码. 实现service 在com.l ...
- [springboot 开发单体web shop] 2. Mybatis Generator 生成common mapper
Mybatis Generator tool 在我们开启一个新项目的研发后,通常要编写很多的entity/pojo/dto/mapper/dao..., 大多研发兄弟们都会抱怨,为什么我要重复写CRU ...
- [springboot 开发单体web shop] 3. 用户注册实现
目录 用户注册 ## 创建数据库 ## 生成UserMapper ## 编写业务逻辑 ## 编写user service UserServiceImpl#findUserByUserName 说明 U ...
- 开发单体web shop] 6. 商品分类和轮播广告展示
目录 商品分类&轮播广告 商品分类|ProductCategory 需求分析 开发梳理 编码实现 轮播广告|SlideAD 需求分析 开发梳理 编码实现 福利讲解 源码下载 下节预告 商品分类 ...
- iOS开发 仿淘宝,京东商品详情3D动画
- (void)show { [[UIApplication sharedApplication].windows[0] addSubview:self.projectView]; CGRect fr ...
随机推荐
- pycharm中拉取新分支代码
将本地代码由主分支切换到新分支 切换成功
- jquery a标签的锚点点击的时候页面上缓慢滚动
a标签增加一个名字触发效果: $(".transition").click(function(){ if (location.pathname.replace(/^\//, '') ...
- Spring Boot 配置 - 配置信息加密
▶ Spring Boot 依赖与配置 Maven 依赖 <dependency> <groupId>org.springframework.boot</groupId& ...
- Newtonsoft—Json.NET常用方法简述
Json.NET常用方法汇总(可解决日常百分之90的需求) 0.Json.NET基础用法 首先去官网下载最新的Newtonsoft.Json.dll(也可以使用VS自带的NuGet搜索Json.NET ...
- 用Java JMC控制台分析线程阻塞原因
问题 今天在玩dianping-CAT框架时,发现请求某个页面的时候,发生了阻塞.浏览器得不到响应. 环境 本地Tomcat 8 , Windows 系统. 解决 启动jmc 控制台,找到BLOCKE ...
- 你好,Go语言
本文是「vangoleo的Go语言学习笔记」系列文章之一. 官网: http://www.vangoleo.com/go/hello-golang/ 我在2015年第一次接触Go语言,完成了Hello ...
- vuex状态管理安装方法
1.可以启动vue ui 手动添加vuex. 或使用 cnpm install vuex 2.使用,import vuex from “vuex” vue.use(vuex) 3.安装插件, 首先键入 ...
- spring整合mybatisplus2.x详解
一丶Mp的配置文件 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="htt ...
- vue项目中v-for渲染失败
在项目中,v-for渲染列表失败,无报错,数组有数据.上网查,好多说是因为动态绑定class的原因,但是经过几番测试,都无效果. 在经过不断尝试,搜索,终于找到原因所在. 问题原因:在v-for循环中 ...
- Java零基础入门之基础语法
一.Java标识符 什么是标识符? 标识符是用来标识类名.对象名.变量名.方法名.数组名.自定义数据类型的有效字符序列. 合法的标识符 ①:由字母.数字.下划线"_".美元符号&q ...