上节 咱们实现了根据搜索关键词查询商品列表和根据商品分类查询,而且使用到了mybatis-pagehelper
插件,讲解了如何使用插件来帮助咱们快速实现分页数据查询。本文咱们将继续开发商品详情页面和商品留言功能的开发。前端
关于商品详情页,和往常同样,咱们先来看一看jd
的示例:
从上面2张图,咱们能够看出来,大致上须要展现给用户的信息。好比:商品图片,名称,价格,等等。在第二张图中,咱们还能够看到有一个商品评价页签
,这些都是咱们本节要实现的内容。java
咱们根据上图(权当是需求文档,不少需求文档写的比这个可能还差劲不少...)分析一下,咱们的开发大体都要关注哪些points
:git
根据咱们梳理出来的信息,接下来开始编码就会很简单了,你们能够根据以前课程讲解的,先自行实现一波,请开始大家的表演~github
由于咱们在实际的数据传输过程当中,不可能直接把咱们的数据库entity
之间暴露到前端,并且咱们商品相关的数据是存储在不一样的数据表中,咱们必需要封装一个ResponseDTO
来对数据进行传递。spring
ProductDetailResponseDTO
包含了商品主表信息,以及图片列表、商品规格(不一样SKU)以及商品具体参数(产地,生产日期等信息)@Data @ToString @Builder @AllArgsConstructor @NoArgsConstructor public class ProductDetailResponseDTO { private Products products; private List<ProductsImg> productsImgList; private List<ProductsSpec> productsSpecList; private ProductsParam productsParam; }
根据咱们以前表的设计,这里使用生成的通用mapper就能够知足咱们的需求。shell
从咱们封装的要传递到前端的ProductDetailResponseDTO
就能够看出,咱们能够根据商品id
分别查询出商品的相关信息,在controller
进行数据封装就能够了,来实现咱们的查询接口。数据库
在com.liferunner.service.IProductService
中添加接口方法:segmentfault
/** * 根据商品id查询商品 * * @param pid 商品id * @return 商品主信息 */ Products findProductByPid(String pid);
接着,在com.liferunner.service.impl.ProductServiceImpl
中添加实现方法:api
@Override @Transactional(propagation = Propagation.SUPPORTS) public Products findProductByPid(String pid) { return this.productsMapper.selectByPrimaryKey(pid); }
直接使用通用mapper根据主键查询就能够了。session
同上,咱们依次来实现图片、规格、以及商品参数相关的编码工做
/** * 根据商品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对数据进行包装,以后再返回到前端,供用户来进行查看,在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()
封装成返回到前端的对象。
按照惯例,写完代码咱们须要进行测试。
{ "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,固然数据的传输对象是咱们须要先来定义的。
建立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; }
在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>
若是没有传递评价级别的话,默认查询所有评价信息。
在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; }
由于评价过多会使用到分页,这里使用通用分页返回结果,关于分页,可查看 学习分页传送门。
在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)
有心的小伙伴确定又注意到了,在Service中处理查询时,我一部分使用了@Transactional(propagation = 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).
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
注解的话,即使你在当前事务中,你也不能对我进行操做和变动。
添加
PROPAGATION.SUPPORTS
以后,当前查询中能够对当前的事务进行设置回调动做,不添加就不行。
下一节咱们将继续开发商品详情展现以及商品评价业务,在过程当中使用到的任何开发组件,我都会经过专门的一节来进行介绍的,兄弟们末慌!
gogogo!