上文回顾

上节 我们实现了根据搜索关键词查询商品列表和根据商品分类查询,并且使用到了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!

[springboot 开发单体web shop] 8. 商品详情&评价展示的更多相关文章

  1. [springboot 开发单体web shop] 7. 多种形式提供商品列表

    上文回顾 上节 我们实现了仿jd的轮播广告以及商品分类的功能,并且讲解了不同的注入方式,本节我们将继续实现我们的电商主业务,商品信息的展示. 需求分析 首先,在我们开始本节编码之前,我们先来分析一下都 ...

  2. [springboot 开发单体web shop] 1. 前言介绍和环境搭建

    前言介绍和环境搭建 简述 springboot 本身是为了做服务化用的,我们为什么要反其道使用它来开发一份单体web应用呢? 在我们现实的开发工作中,还有大量的业务系统使用的是单体应用,特别是对于中小 ...

  3. [springboot 开发单体web shop] 6. 商品分类和轮播广告展示

    商品分类&轮播广告 因最近又被困在了OSGI技术POC,更新进度有点慢,希望大家不要怪罪哦. 上节 我们实现了登录之后前端的展示,如: 接着,我们来实现左侧分类栏目的功能. ## 商品分类|P ...

  4. [springboot 开发单体web shop] 4. Swagger生成Javadoc

    Swagger生成JavaDoc 在日常的工作中,特别是现在前后端分离模式之下,接口的提供造成了我们前后端开发人员的沟通 成本大量提升,因为沟通不到位,不及时而造成的[撕币]事件都成了日常工作.特别是 ...

  5. [springboot 开发单体web shop] 5. 用户登录及首页展示

    用户登录及前端展示 用户登录 在之前的文章中我们实现了用户注册和验证功能,接下来我们继续实现它的登录,以及登录成功之后要在页面上显示的信息. 接下来,我们来编写代码. 实现service 在com.l ...

  6. [springboot 开发单体web shop] 2. Mybatis Generator 生成common mapper

    Mybatis Generator tool 在我们开启一个新项目的研发后,通常要编写很多的entity/pojo/dto/mapper/dao..., 大多研发兄弟们都会抱怨,为什么我要重复写CRU ...

  7. [springboot 开发单体web shop] 3. 用户注册实现

    目录 用户注册 ## 创建数据库 ## 生成UserMapper ## 编写业务逻辑 ## 编写user service UserServiceImpl#findUserByUserName 说明 U ...

  8. 开发单体web shop] 6. 商品分类和轮播广告展示

    目录 商品分类&轮播广告 商品分类|ProductCategory 需求分析 开发梳理 编码实现 轮播广告|SlideAD 需求分析 开发梳理 编码实现 福利讲解 源码下载 下节预告 商品分类 ...

  9. iOS开发 仿淘宝,京东商品详情3D动画

    - (void)show { [[UIApplication sharedApplication].windows[0] addSubview:self.projectView]; CGRect fr ...

随机推荐

  1. Redis 消息队列的实现

    概述 Redis实现消息队列有两种形式: 广播订阅模式:基于Redis的 Pub/Sub 机制,一旦有客户端往某个key里面 publish一个消息,所有subscribe的客户端都会触发事件 集群订 ...

  2. The usage of Markdown---标题

    更新时间:2019.09.14 目录: 1. 序言 2. 标题   2.1 类Atx形式   2.2 类Setext形式 3. 总结 1. 序言   Markdown是一种纯文本的标记语言,只要熟悉M ...

  3. redis入门(二)

    目录 redis入门(二) 前言 持久化 RDB AOF 持久化文件加载 高可用 哨兵 流程 安装部署 配置技巧 集群 原理 集群搭建 参考文档 redis入门(二) 前言 在redis入门(一)简单 ...

  4. django-模板之now标签(七)

    1.在settings.py中设置成中国时区 2.index.html 3.显示

  5. SpringBoot整合MybatisPlus3.X之Sequence(二)

    数据库脚本 DELETE FROM user; ​ INSERT INTO user (id, name, age, email) VALUES (, , 'test1@baomidou.com'), ...

  6. mysql中if函数的正确使用姿势

    --为了今天要写的内容,运行了将近7个小时的程序,在数据库中存储了1千万条数据.-- 今天要说的是mysql数据库的IF()函数的一个实例. 具体场景如下, 先看看表结构: CREATE TABLE ...

  7. spring cloud 2.x版本 Zuul路由网关教程

    前言 本文采用Spring cloud本文为2.1.8RELEASE,version=Greenwich.SR3 本文基于前两篇文章eureka-server.eureka-client.eureka ...

  8. class定义类 及 实现继承

    class 定义类 代码如下: class Student { constructor(name) { this.name = name; } sayHello() { console.log(&qu ...

  9. 10、pytest -- skip和xfail标记

    目录 1. 跳过测试用例的执行 1.1. @pytest.mark.skip装饰器 1.2. pytest.skip方法 1.3. @pytest.mark.skipif装饰器 1.4. pytest ...

  10. 11 一步一步Zabbix4.4.0系统教你实现sendEmail邮件报警

    点击返回:自学Zabbix之路 点击返回:自学Zabbix4.0之路 点击返回:自学zabbix集锦 一步一步Zabbix4.4.0系统教你实现sendEmail邮件报警 sendEmail是一个轻量 ...