[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!