1. 商品详情 当用户搜索到商品,肯定会点击查看,就会进入商品详情页,接下来我们完成商品详情页的展示。
商品详情浏览量比较大,并发高,我们会独立开启一个微服务,用来展示商品详情。
1.1. 创建module 
 
pom.xml依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 <?xml version="1.0"  encoding="UTF-8" ?> <project  xmlns ="http://maven.apache.org/POM/4.0.0"  xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance"           xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" >     <modelVersion > 4.0.0</modelVersion >      <parent >          <groupId > com.atguigu</groupId >          <artifactId > gmall</artifactId >          <version > 0.0.1-SNAPSHOT</version >      </parent >      <groupId > com.atguigu</groupId >      <artifactId > gmall-item</artifactId >      <version > 0.0.1-SNAPSHOT</version >      <name > gmall-item</name >      <description > 谷粒商城商品详情页</description >      <properties >          <java.version > 1.8</java.version >      </properties >      <dependencies >          <dependency >              <groupId > com.atguigu</groupId >              <artifactId > gmall-pms-interface</artifactId >              <version > 0.0.1-SNAPSHOT</version >          </dependency >          <dependency >              <groupId > com.atguigu</groupId >              <artifactId > gmall-wms-interface</artifactId >              <version > 0.0.1-SNAPSHOT</version >          </dependency >          <dependency >              <groupId > com.atguigu</groupId >              <artifactId > gmall-sms-interface</artifactId >              <version > 0.0.1-SNAPSHOT</version >          </dependency >          <dependency >              <groupId > org.springframework.boot</groupId >              <artifactId > spring-boot-starter-web</artifactId >          </dependency >          <dependency >              <groupId > com.alibaba.cloud</groupId >              <artifactId > spring-cloud-starter-alibaba-nacos-config</artifactId >          </dependency >          <dependency >              <groupId > com.alibaba.cloud</groupId >              <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId >          </dependency >          <dependency >              <groupId > com.alibaba.cloud</groupId >              <artifactId > spring-cloud-starter-alibaba-sentinel</artifactId >          </dependency >          <dependency >              <groupId > org.springframework.cloud</groupId >              <artifactId > spring-cloud-starter-openfeign</artifactId >          </dependency >          <dependency >              <groupId > org.springframework.cloud</groupId >              <artifactId > spring-cloud-starter-zipkin</artifactId >          </dependency >          <dependency >              <groupId > org.springframework.boot</groupId >              <artifactId > spring-boot-starter-test</artifactId >              <scope > test</scope >              <exclusions >                  <exclusion >                      <groupId > org.junit.vintage</groupId >                      <artifactId > junit-vintage-engine</artifactId >                  </exclusion >              </exclusions >          </dependency >      </dependencies >      <build >          <plugins >              <plugin >                  <groupId > org.springframework.boot</groupId >                  <artifactId > spring-boot-maven-plugin</artifactId >              </plugin >          </plugins >      </build >  </project > 
bootstrap.properties:
1 2 3 4 5 6 7 spring:   application:      name:  item-service    cloud:      nacos:        config:          server-addr:  127.0 .0 .1 :8848  
application.properties:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 server:   port:  18088  spring:   cloud:      nacos:        discovery:          server-addr:  127.0 .0 .1 :8848      sentinel:        transport:          dashboard:  localhost:8080          port:  8719    zipkin:      base-url:  http://localhost:9411      discovery-client-enabled:  false      sender:        type:  web    sleuth:      sampler:        probability:  1    redis:      host:  172.16 .116 .100  feign:   sentinel:      enabled:  true  logging:   level:      com.atguigu.gmall:  debug  
GmallItemApplication:
1 2 3 4 5 6 7 8 9 10 @SpringBootApplication @EnableDiscoveryClient @EnableFeignClients public  class  GmallItemApplication  {    public  static  void  main (String[] args)  {         SpringApplication.run(GmallItemApplication.class, args);     } } 
别忘了配置网关:
1.2. 数据模型 当点击搜索列表中的一个记录,会跳转到商品详情页。这个商品详情页是一个spu?还是sku ?
以京东为例:
在京东搜索小米,出现搜索列表后。点击其中一条记录,跳转到商品详情页,这个商品详情页展示的是:
结合页面,商品详情页需要的数据有:
面包屑信息:
sku相关信息:
sku的基本信息(标题、副标题、价格、大图片等) 
sku的所有图片 
sku的所有促销信息 
sku的库存情况(是否有货) 
 
spu下所有销售组合:
每个销售属性可取值集合,方便渲染可选值列表 
当前商品的销售属性,方便渲染选中项 
销售属性组合与skuId的映射关系,方便切换sku 
 
商品详情信息:
spu的描述信息 
spu的所有基本规格分组及规格参数 
 
最终设计如下:
 
商品详情页总的数据模型:ItemVO
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 @Data public  class  ItemVo  {         private  List<CategoryEntity> categories;          private  Long brandId;     private  String brandName;          private  Long spuId;     private  String spuName;          private  Long skuId;     private  String title;     private  String subTitle;     private  BigDecimal price;     private  Integer weight;     private  String defaltImage;          private  List<SkuImagesEntity> images;          private  List<ItemSaleVo> sales;          private  Boolean  store  =  false ;                         private  List<SaleAttrValueVo> saleAttrs;          private  Map<Long, String> saleAttr;          private  String skusJson;          private  List<String> spuImages;          private  List<ItemGroupVo> groups; } 
ItemSaleVo:
1 2 3 4 5 @Data public  class  ItemSaleVo  {    private  String type;      private  String desc;  } 
ItemGroupVo:
1 2 3 4 5 6 @Data public  class  ItemGroupVo  {    private  String groupName;     private  List<AttrValueVo> attrValues; } 
AttrValueVo:
1 2 3 4 5 6 7 @Data public  class  AttrValueVo  {    private  Long attrId;     private  String attrName;     private  String attrValue; } 
SaleAttrValueVo:
1 2 3 4 5 6 7 @Data public  class  SaleAttrValueVo  {    private  Long attrId;     private  String attrName;     private  Set<String> attrValues; } 
1.3. 远程调用接口 跳转到商品详情页时,已知条件只有一个skuId。为渲染商品详情页,需要的远程数据结构提供数据:
根据skuId查询sku(已有)
根据sku中的三级分类id查询一二三级分类
根据sku中的品牌id查询品牌(已有)
根据sku中的spuId查询spu信息(已有)
根据skuId查询sku所有图片
根据skuId查询sku的所有营销信息
根据skuId查询sku的库存信息(已有)
根据sku中的spuId查询spu下的所有销售属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 [   {  	attrId:  3 ,  	attrName:  '颜色',  	attrValues:  [ '白色',  '黑色',  '粉色']    } ,    {  	attrId:  8 ,  	attrName:  '内存',  	attrValues:  [ '6 G',  '8 G',  '12 G']    } ,    {  	attrId:  9 ,  	attrName:  '存储',  	attrValues:  [ '128 G',  '256 G',  '512 G']    }  ] 
根据skuId查询当前sku的销售属性
根据sku中的spuId查询spu下所有sku:销售属性组合与skuId映射关系
1 { '白色, 8 G, 128 G':  4 ,  '白色, 8 G, 256 G':  5 ,  '白色, 8 G, 512 G':  6 ,  '白色, 12 G, 128 G':  7 } 
根据sku中spuId查询spu的描述信息(已有)
根据分类id、spuId及skuId查询分组及组下的规格参数值
 
1.3.1. 添加远程接口 这些数据模型需要调用远程接口从其他微服务获取,所以这里先编写feign接口
 
GmallPmsClient:
1 2 3 @FeignClient("pms-service") public  interface  GmallPmsClient  extends  GmallPmsApi  {} 
GmallSmsClient:
1 2 3 @FeignClient("sms-service") public  interface  GmallSmsClient  extends  GmallSmsApi  {} 
GmallWmsClient:
1 2 3 @FeignClient("wms-service") public  interface  GmallWmsClient  extends  GmallWmsApi  {} 
有些接口已经有了,直接给GmallPmsApi添加方法即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @GetMapping("pms/sku/{id}") public  ResponseVo<SkuEntity> querySkuById (@PathVariable("id")  Long id) ;@GetMapping("pms/spu/{id}") public  ResponseVo<SpuEntity> querySpuById (@PathVariable("id")  Long id) ;@GetMapping("pms/spudesc/{spuId}") public  ResponseVo<SpuDescEntity> querySpuDescById (@PathVariable("spuId")  Long spuId) ;
1.3.2. 根据三级分类id查询一二三级分类 在CategoryController中添加方法:
1 2 3 4 5 @GetMapping("all/{cid3}") public  ResponseVo<List<CategoryEntity>> queryCategoriesByCid3 (@PathVariable("cid3") Long cid3) {    List<CategoryEntity> itemCategoryVos = this .categoryService.queryCategoriesByCid3(cid3);     return  ResponseVo.ok(itemCategoryVos); } 
在CategoryService接口中添加方法:
1 List<CategoryEntity> queryCategoriesByCid3 (Long cid3) ; 
在CategoryServiceImpl实现类中添加方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Override public  List<CategoryEntity> queryCategoriesByCid3 (Long cid3)  {         CategoryEntity  categoryEntity3  =  this .categoryMapper.selectById(cid3);          CategoryEntity  categoryEntity2  =  this .categoryMapper.selectById(categoryEntity3.getParentId());          CategoryEntity  categoryEntity1  =  this .categoryMapper.selectById(categoryEntity2.getParentId());     return  Arrays.asList(categoryEntity1, categoryEntity2, categoryEntity3); } 
在GmallPmsApi中添加接口方法:
1 2 @GetMapping("pms/category/all/{cid3}") public  ResponseVo<List<CategoryEntity>> queryCategoriesByCid3 (@PathVariable("cid3") Long cid3) ;
1.3.3. 根据skuId查询sku的图片 在SkuImagesController中添加:
1 2 3 4 5 6 @GetMapping("sku/{skuId}") public  ResponseVo<List<SkuImagesEntity>> queryImagesBySkuId (@PathVariable("skuId")  Long skuId) {    List<SkuImagesEntity> imagesEntities = this .skuImagesService.list(new  QueryWrapper <SkuImagesEntity>().eq("sku_id" , skuId));     return  ResponseVo.ok(imagesEntities); } 
在GmallPmsApi中添加接口方法:
1 2 @GetMapping("pms/skuimages/sku/{skuId}") public  ResponseVo<List<SkuImagesEntity>> queryImagesBySkuId (@PathVariable("skuId") Long skuId) ;
1.3.4. 查询sku的营销信息 在SkuBoundsController中添加查询营销信息的方法:
1 2 3 4 5 @GetMapping("sku/{skuId}") public  ResponseVo<List<ItemSaleVo>> querySalesBySkuId (@PathVariable("skuId") Long skuId) {    List<ItemSaleVo> itemSaleVos = this .skuBoundsService.querySalesBySkuId(skuId);     return  ResponseVo.ok(itemSaleVos); } 
在SkuBoundsService中添加接口方法:
1 List<ItemSaleVo> querySalesBySkuId (Long skuId) ; 
在SkuBoundsServiceImpl中实现接口方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Override public  List<ItemSaleVo> querySalesBySkuId (Long skuId)  {    List<ItemSaleVo> itemSaleVos = new  ArrayList <>();          SkuBoundsEntity  skuBoundsEntity  =  this .getOne(new  QueryWrapper <SkuBoundsEntity>().eq("sku_id" , skuId));     ItemSaleVo  bounds  =  new  ItemSaleVo ();     bounds.setType("积分" );     bounds.setDesc("送"  + skuBoundsEntity.getGrowBounds() + "成长积分,送"  + skuBoundsEntity.getBuyBounds() + "购物积分" );     itemSaleVos.add(bounds);          SkuFullReductionEntity  reductionEntity  =  this .reductionMapper.selectOne(new  QueryWrapper <SkuFullReductionEntity>().eq("sku_id" , skuId));     ItemSaleVo  reduction  =  new  ItemSaleVo ();     reduction.setType("满减" );     reduction.setDesc("满"  + reductionEntity.getFullPrice() + "减"  + reductionEntity.getReducePrice());     itemSaleVos.add(reduction);          SkuLadderEntity  ladderEntity  =  this .ladderMapper.selectOne(new  QueryWrapper <SkuLadderEntity>().eq("sku_id" , skuId));     ItemSaleVo  ladder  =  new  ItemSaleVo ();     ladder.setType("打折" );     ladder.setDesc("满"  + ladderEntity.getFullCount() + "件打"  + ladderEntity.getDiscount().divide(new  BigDecimal (10 )) + "折" );     itemSaleVos.add(ladder);     return  itemSaleVos; } 
在GmallSmsApi中添加数据接口方法
1 2 @GetMapping("sms/skubounds/sku/{skuId}") public  ResponseVo<List<ItemSaleVo>> querySalesBySkuId (@PathVariable("skuId") Long skuId) ;
1.3.5. 根据spuId查询spu下的所有销售属性 SkuAttrValueController中添加查询方法:
1 2 3 4 5 @GetMapping("spu/{spuId}") public  ResponseVo<List<SaleAttrValueVo>> querySkuAttrValuesBySpuId (@PathVariable("spuId") Long spuId) {    List<SaleAttrValueVo> saleAttrValueVos = this .skuAttrValueService.querySkuAttrValuesBySpuId(spuId);     return  ResponseVo.ok(saleAttrValueVos); } 
在SkuSaleAttrValueService接口中添加抽象方法:
1 List<SaleAttrValueVo> querySkuAttrValuesBySpuId (Long spuId) ; 
在SkuSaleAttrValueServiceImpl中实现该方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Autowired private  SkuAttrValueMapper skuAttrValueMapper;@Override public  List<SaleAttrValueVo> querySkuAttrValuesBySpuId (Long spuId)  {    List<AttrValueVo> attrValueVos = skuAttrValueMapper.querySkuAttrValuesBySpuId(spuId);          Map<Long, List<AttrValueVo>> map = attrValueVos.stream().collect(groupingBy(AttrValueVo::getAttrId));          List<SaleAttrValueVo> saleAttrValueVos = new  ArrayList <>();     map.forEach((attrId, attrs) -> {         SaleAttrValueVo  saleAttrValueVo  =  new  SaleAttrValueVo ();                  saleAttrValueVo.setAttrId(attrId);                  saleAttrValueVo.setAttrName(attrs.get(0 ).getAttrName());                  Set<String> attrValues = attrs.stream().map(AttrValueVo::getAttrValue).collect(Collectors.toSet());         saleAttrValueVo.setAttrValues(attrValues);         saleAttrValueVos.add(saleAttrValueVo);     });     return  saleAttrValueVos; } 
SkuAttrValueMapper接口添加方法:
1 2 3 4 5 @Mapper public  interface  SkuAttrValueMapper  extends  BaseMapper <SkuAttrValueEntity> {    List<AttrValueVo> querySkuAttrValuesBySpuId (Long spuId) ; } 
SkuAttrValueMapper.xml中配置映射:
1 2 3 4 5 6 7 8 9 10 11 <?xml version="1.0"  encoding="UTF-8" ?> <!DOCTYPE mapper  PUBLIC  "-//mybatis.org//DTD Mapper 3.0//EN"  "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper  namespace ="com.atguigu.gmall.pms.mapper.SkuAttrValueMapper" >     <select  id ="querySkuAttrValuesBySpuId"  resultType ="com.atguigu.gmall.pms.vo.AttrValueVo" >          select a.attr_id,attr_name,a.attr_value             from pms_sku_attr_value a INNER JOIN pms_sku b on a.sku_id=b.id         where spu_id=#{spuId};     </select >  </mapper > 
在GmallPmsApi中添加数据接口:
1 2 @GetMapping("pms/skuattrvalue/spu/{spuId}") public  ResponseVo<List<SaleAttrValueVo>> querySkuAttrValuesBySpuId (@PathVariable("spuId") Long spuId) ;
1.3.6. 查询spu下skuId及销售属性的关系 给SkuAttrValueController添加方法:
1 2 3 4 5 @GetMapping("spu/sku/{spuId}") public  ResponseVo<String> querySkusJsonBySpuId (@PathVariable("spuId")  Long spuId) {    String  skusJson  =  this .skuAttrValueService.querySkusJsonBySpuId(spuId);     return  ResponseVo.ok(skusJson); } 
给SkuAttrValueService接口添加方法:
1 String querySkusJsonBySpuId (Long spuId) ; 
SkuAttrValueServiceImpl实现类实现接口方法:
1 2 3 4 5 6 7 8 @Override public  String querySkusJsonBySpuId (Long spuId)  {         List<Map<String, Object>> skus = this .skuAttrValueMapper.querySkusJsonBySpuId(spuId);          Map<String, Long> map = skus.stream().collect(Collectors.toMap(sku -> sku.get("attr_values" ).toString(), sku -> (Long) sku.get("sku_id" )));     return  JSON.toJSONString(map); } 
给SkuAttrValueMapper添加querySkusJsonBySpuId方法:
1 2 3 4 5 6 7 @Mapper public  interface  SkuAttrValueMapper  extends  BaseMapper <SkuAttrValueEntity> {    List<AttrValueVo> querySkuAttrValuesBySpuId (Long spuId) ;     List<Map<String, Object>> querySkusJsonBySpuId (Long spuId) ; } 
给SkuAttrValueMapper.xml映射文件添加映射:
1 2 3 4 5 <select  id ="querySkusJsonBySpuId"  resultType ="hashmap" >     select GROUP_CONCAT(a.attr_value) as attr_values, a.sku_id     from pms_sku_attr_value a INNER JOIN pms_sku b on a.sku_id=b.id     where b.spu_id=#{spuId} group by a.sku_id </select > 
给GmallPmsApi添加接口方法:
1 2 @GetMapping("pms/skuattrvalue/spu/sku/{spuId}") public  ResponseVo<String> querySkusJsonBySpuId (@PathVariable("spuId")  Long spuId) ;
1.3.7. 查询组及组下参数和值 在AttrGroupController中添加方法:
1 2 3 4 5 6 7 8 9 @GetMapping("withattrvalues") public  ResponseVo<List<ItemGroupVo>> queryGroupsBySpuIdAndCid (     @RequestParam("spuId") Long spuId,     @RequestParam("skuId") Long skuId,     @RequestParam("cid") Long cid     ) {        List<ItemGroupVo> itemGroupVOS = attrGroupService.queryGroupsBySpuIdAndCid(spuId, skuId, cid);         return  ResponseVo.ok(itemGroupVOS);     } 
在AttrGroupService接口中添加接口方法:
1 List<ItemGroupVo> queryGroupsBySpuIdAndCid (Long spuId, Long skuId, Long cid) ; 
在AttrGroupServiceImpl实现类中添加实现方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 @Autowired private  SpuAttrValueMapper spuAttrValueMapper;@Autowired private  SkuAttrValueMapper skuAttrValueMapper;@Override public  List<ItemGroupVo> queryGroupsBySpuIdAndCid (Long spuId, Long skuId, Long cid)  {         List<AttrGroupEntity> attrGroupEntities = this .list(new  QueryWrapper <AttrGroupEntity>().eq("category_id" , cid));     if  (CollectionUtils.isEmpty(attrGroupEntities)){         return  null ;     }          return  attrGroupEntities.stream().map(group -> {         ItemGroupVo  itemGroupVo  =  new  ItemGroupVo ();         itemGroupVo.setGroupId(group.getId());         itemGroupVo.setGroupName(group.getName());         List<AttrEntity> attrEntities = this .attrMapper.selectList(new  QueryWrapper <AttrEntity>().eq("group_id" , group.getId()));         if  (!CollectionUtils.isEmpty(attrEntities)){             List<Long> attrIds = attrEntities.stream().map(AttrEntity::getId).collect(Collectors.toList());                          List<SpuAttrValueEntity> spuAttrValueEntities = this .spuAttrValueMapper.selectList(new  QueryWrapper <SpuAttrValueEntity>().eq("spu_id" , spuId).in("attr_id" , attrIds));             List<SkuAttrValueEntity> skuAttrValueEntities = this .skuAttrValueMapper.selectList(new  QueryWrapper <SkuAttrValueEntity>().eq("sku_id" , skuId).in("attr_id" , attrIds));             List<AttrValueVo> attrValueVos = new  ArrayList <>();             if  (!CollectionUtils.isEmpty(spuAttrValueEntities)){                 List<AttrValueVo> spuAttrValueVos = spuAttrValueEntities.stream().map(attrValue -> {                     AttrValueVo  attrValueVo  =  new  AttrValueVo ();                     BeanUtils.copyProperties(attrValue, attrValueVo);                     return  attrValueVo;                 }).collect(Collectors.toList());                 attrValueVos.addAll(spuAttrValueVos);             }             if  (!CollectionUtils.isEmpty(skuAttrValueEntities)){                 List<AttrValueVo> skuAttrValueVos = skuAttrValueEntities.stream().map(attrValue -> {                     AttrValueVo  attrValueVo  =  new  AttrValueVo ();                     BeanUtils.copyProperties(attrValue, attrValueVo);                     return  attrValueVo;                 }).collect(Collectors.toList());                 attrValueVos.addAll(skuAttrValueVos);             }             itemGroupVo.setAttrValues(attrValueVos);         }         return  itemGroupVo;     }).collect(Collectors.toList()); } 
1.4. 给商品详情页提供数据 请求路径:/item/{skuId}
请求参数:skuId
请求方式:GET
响应:ItemVO的json数据
 
ItemController:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RestController @RequestMapping("item") public  class  ItemController  {    @Autowired      private  ItemService itemService;     @GetMapping("{skuId}")      public  ResponseVo<ItemVo> load (@PathVariable("skuId") Long skuId) {         ItemVo  itemVo  =  this .itemService.load(skuId);         return  ResponseVo.ok(itemVo);     } } 
ItemService:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 @Service public  class  ItemService  {    @Autowired      private  GmallPmsClient pmsClient;     @Autowired      private  GmallWmsClient wmsClient;     @Autowired      private  GmallSmsClient smsClient;     @Autowired      private  ThreadPoolExecutor threadPoolExecutor;     public  ItemVo load (Long skuId)  {         ItemVo  itemVo  =  new  ItemVo ();                  ResponseVo<SkuEntity> skuEntityResponseVo = this .pmsClient.querySkuById(skuId);         SkuEntity  skuEntity  =  skuEntityResponseVo.getData();         if  (skuEntity == null ) {             return  null ;         }         itemVo.setSkuId(skuId);         itemVo.setTitle(skuEntity.getTitle());         itemVo.setSubTitle(skuEntity.getSubtitle());         itemVo.setPrice(skuEntity.getPrice());         itemVo.setWeight(skuEntity.getWeight());         itemVo.setDefaltImage(skuEntity.getDefaultImage());                  ResponseVo<List<CategoryEntity>> categoryResponseVo = this .pmsClient.queryCategoriesByCid3(skuEntity.getCategoryId());         List<CategoryEntity> categoryEntities = categoryResponseVo.getData();         itemVo.setCategories(categoryEntities);                  ResponseVo<BrandEntity> brandEntityResponseVo = this .pmsClient.queryBrandById(skuEntity.getBrandId());         BrandEntity  brandEntity  =  brandEntityResponseVo.getData();         if  (brandEntity != null ) {             itemVo.setBrandId(brandEntity.getId());             itemVo.setBrandName(brandEntity.getName());         }                  ResponseVo<SpuEntity> spuEntityResponseVo = this .pmsClient.querySpuById(skuEntity.getSpuId());         SpuEntity  spuEntity  =  spuEntityResponseVo.getData();         if  (spuEntity != null ) {             itemVo.setSpuId(spuEntity.getId());             itemVo.setSpuName(spuEntity.getName());         }                  ResponseVo<List<SkuImagesEntity>> skuImagesResponseVo = this .pmsClient.queryImagesBySkuId(skuId);         List<SkuImagesEntity> skuImagesEntities = skuImagesResponseVo.getData();         itemVo.setImages(skuImagesEntities);                  ResponseVo<List<ItemSaleVo>> salesResponseVo = this .smsClient.querySalesBySkuId(skuId);         List<ItemSaleVo> sales = salesResponseVo.getData();         itemVo.setSales(sales);                  ResponseVo<List<WareSkuEntity>> wareSkuResponseVo = this .wmsClient.queryWareSkusBySkuId(skuId);         List<WareSkuEntity> wareSkuEntities = wareSkuResponseVo.getData();         if  (!CollectionUtils.isEmpty(wareSkuEntities)) {             itemVo.setStore(wareSkuEntities.stream().anyMatch(wareSkuEntity -> wareSkuEntity.getStock() - wareSkuEntity.getStockLocked() > 0 ));         }                  ResponseVo<List<SaleAttrValueVo>> saleAttrValueVoResponseVo = this .pmsClient.querySkuAttrValuesBySpuId(skuEntity.getSpuId());         List<SaleAttrValueVo> saleAttrValueVos = saleAttrValueVoResponseVo.getData();         itemVo.setSaleAttrs(saleAttrValueVos);                  ResponseVo<List<SkuAttrValueEntity>> saleAttrResponseVo = this .pmsClient.querySkuAttrValuesBySkuId(skuId);         List<SkuAttrValueEntity> skuAttrValueEntities = saleAttrResponseVo.getData();         Map<Long, String> map = skuAttrValueEntities.stream().collect(Collectors.toMap(SkuAttrValueEntity::getAttrId, SkuAttrValueEntity::getAttrValue));         itemVo.setSaleAttr(map);                  ResponseVo<String> skusJsonResponseVo = this .pmsClient.querySkusJsonBySpuId(skuEntity.getSpuId());         String  skusJson  =  skusJsonResponseVo.getData();         itemVo.setSkusJson(skusJson);                  ResponseVo<SpuDescEntity> spuDescEntityResponseVo = this .pmsClient.querySpuDescById(skuEntity.getSpuId());         SpuDescEntity  spuDescEntity  =  spuDescEntityResponseVo.getData();         if  (spuDescEntity != null  && StringUtils.isNotBlank(spuDescEntity.getDecript())) {             String[] images = StringUtils.split(spuDescEntity.getDecript(), "," );             itemVo.setSpuImages(Arrays.asList(images));         }                  ResponseVo<List<ItemGroupVo>> groupResponseVo = this .pmsClient.queryGoupsWithAttrValues(skuEntity.getCategoryId(), skuEntity.getSpuId(), skuId);         List<ItemGroupVo> itemGroupVos = groupResponseVo.getData();         itemVo.setGroups(itemGroupVos);         return  itemVo;     } } 
完成!!
2. CompletableFuture异步调用 问题:查询商品详情页的逻辑非常复杂,数据的获取都需要远程调用,必然需要花费更多的时间。
假如商品详情页的每个查询,需要如下标注的时间才能完成
1 2 3 4 5 6 7 8 9 10 11 12 13 ......... 
那么,用户需要6.5s后才能看到商品详情页的内容。很显然是不能接受的。
如果有多个线程同时完成这6步操作,也许只需要1.5s即可完成响应。
2.1. 线程回顾 初始化线程的4种方式:
继承Thread 
实现Runnable接口 
实现Callable接口 + FutureTask (可以拿到返回结果,可以处理异常) 
线程池 
 
方式1和方式2:主进程无法获取线程的运算结果。不适合当前场景
方式3:主进程可以获取线程的运算结果,并设置给itemVO,但是不利于控制服务器中的线程资源。可以导致服务器资源耗尽。
方式4:通过如下两种方式初始化线程池:
1 2 3 Executors.newFiexedThreadPool(3 ); new  ThreadPoolExecutor (corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit unit, workQueue, threadFactory, handler);
通过线程池性能稳定,也可以获取执行结果,并捕获异常。但是,在业务复杂情况下,一个异步调用可能会依赖于另一个异步调用的执行结果。
2.2. CompletableFuture介绍 Future是Java 5添加的类,用来描述一个异步计算的结果。你可以使用isDone方法检查计算是否完成,或者使用get阻塞住调用线程,直到计算完成返回结果,你也可以使用cancel方法停止任务的执行。
虽然Future以及相关使用方法提供了异步执行任务的能力,但是对于结果的获取却是很不方便,只能通过阻塞或者轮询的方式得到任务的结果。阻塞的方式显然和我们的异步编程的初衷相违背,轮询的方式又会耗费无谓的CPU资源,而且也不能及时地得到计算结果,为什么不能用观察者设计模式当计算结果完成及时通知监听者呢?
很多语言,比如Node.js,采用回调的方式实现异步编程。Java的一些框架,比如Netty,自己扩展了Java的 Future接口,提供了addListener等多个扩展方法;Google guava也提供了通用的扩展Future;Scala也提供了简单易用且功能强大的Future/Promise异步编程模式。
作为正统的Java类库,是不是应该做点什么,加强一下自身库的功能呢?
在Java 8中, 新增加了一个包含50个方法左右的类: CompletableFuture,提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,提供了函数式编程的能力,可以通过回调的方式处理计算结果,并且提供了转换和组合CompletableFuture的方法。
CompletableFuture类实现了Future接口,所以你还是可以像以前一样通过get方法阻塞或者轮询的方式获得结果,但是这种方式不推荐使用。
CompletableFuture和FutureTask同属于Future接口的实现类,都可以获取线程的执行结果。
2.3. 创建异步对象 CompletableFuture 提供了四个静态方法来创建一个异步操作。
1 2 3 4 static  CompletableFuture<Void> runAsync (Runnable runnable) public  static  CompletableFuture<Void> runAsync (Runnable runnable, Executor executor) public  static  <U> CompletableFuture<U> supplyAsync (Supplier<U> supplier) public  static  <U> CompletableFuture<U> supplyAsync (Supplier<U> supplier, Executor executor) 
没有指定Executor的方法会使用ForkJoinPool.commonPool() 作为它的线程池执行异步代码 。如果指定线程池,则使用指定的线程池运行。以下所有的方法都类同。
runAsync方法不支持返回值。 
supplyAsync可以支持返回值。 
 
2.4. 计算完成时回调方法 当CompletableFuture的计算结果完成,或者抛出异常的时候,可以执行特定的Action。主要是下面的方法:
1 2 3 4 5 public  CompletableFuture<T> whenComplete (BiConsumer<? super  T,? super  Throwable> action) ;public  CompletableFuture<T> whenCompleteAsync (BiConsumer<? super  T,? super  Throwable> action) ;public  CompletableFuture<T> whenCompleteAsync (BiConsumer<? super  T,? super  Throwable> action, Executor executor) ;public  CompletableFuture<T> exceptionally (Function<Throwable,? extends T> fn) ;
whenComplete可以处理正常和异常的计算结果,exceptionally处理异常情况。BiConsumer<? super T,? super Throwable>可以定义处理业务
whenComplete 和 whenCompleteAsync 的区别:
方法不以Async结尾,意味着Action使用相同的线程执行,而Async可能会使用其他线程执行(如果是使用相同的线程池,也可能会被同一个线程选中执行) 
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public  class  CompletableFutureDemo  {    public  static  void  main (String[] args)  throws  ExecutionException, InterruptedException {         CompletableFuture  future  =  CompletableFuture.supplyAsync(new  Supplier <Object>() {             @Override              public  Object get ()  {                 System.out.println(Thread.currentThread().getName() + "\t completableFuture" );                 int  i  =  10  / 0 ;                 return  1024 ;             }         }).whenComplete(new  BiConsumer <Object, Throwable>() {             @Override              public  void  accept (Object o, Throwable throwable)  {                 System.out.println("-------o="  + o.toString());                 System.out.println("-------throwable="  + throwable);             }         }).exceptionally(new  Function <Throwable, Object>() {             @Override              public  Object apply (Throwable throwable)  {                 System.out.println("throwable="  + throwable);                 return  6666 ;             }         });         System.out.println(future.get());     } } 
2.5. 线程串行化方法 thenApply 方法:当一个线程依赖另一个线程时,获取上一个任务返回的结果,并返回当前任务的返回值。
thenAccept方法:消费处理结果。接收任务的处理结果,并消费处理,无返回结果。
thenRun方法:只要上面的任务执行完成,就开始执行thenRun,只是处理完任务后,执行 thenRun的后续操作
带有Async默认是异步执行的。这里所谓的异步指的是不在当前线程内执行。
1 2 3 4 5 6 7 8 9 10 11 public  <U> CompletableFuture<U> thenApply (Function<? super  T,? extends U> fn) public  <U> CompletableFuture<U> thenApplyAsync (Function<? super  T,? extends U> fn) public  <U> CompletableFuture<U> thenApplyAsync (Function<? super  T,? extends U> fn, Executor executor) public  CompletionStage<Void> thenAccept (Consumer<? super  T> action) ;public  CompletionStage<Void> thenAcceptAsync (Consumer<? super  T> action) ;public  CompletionStage<Void> thenAcceptAsync (Consumer<? super  T> action,Executor executor) ;public  CompletionStage<Void> thenRun (Runnable action) ;public  CompletionStage<Void> thenRunAsync (Runnable action) ;public  CompletionStage<Void> thenRunAsync (Runnable action,Executor executor) ;
Function<? super T,? extends U>
代码演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public  static  void  main (String[] args)  throws  ExecutionException, InterruptedException {    CompletableFuture<Integer> future = CompletableFuture.supplyAsync(new  Supplier <Integer>() {         @Override          public  Integer get ()  {             System.out.println(Thread.currentThread().getName() + "\t completableFuture" );                          return  1024 ;         }     }).thenApply(new  Function <Integer, Integer>() {         @Override          public  Integer apply (Integer o)  {             System.out.println("thenApply方法,上次返回结果:"  + o);             return   o * 2 ;         }     }).whenComplete(new  BiConsumer <Integer, Throwable>() {         @Override          public  void  accept (Integer o, Throwable throwable)  {             System.out.println("-------o="  + o);             System.out.println("-------throwable="  + throwable);         }     }).exceptionally(new  Function <Throwable, Integer>() {         @Override          public  Integer apply (Throwable throwable)  {             System.out.println("throwable="  + throwable);             return  6666 ;         }     });     System.out.println(future.get()); } 
2.6. 多任务组合 1 2 3 public  static  CompletableFuture<Void> allOf (CompletableFuture<?>... cfs) ;public  static  CompletableFuture<Object> anyOf (CompletableFuture<?>... cfs) ;
allOf:等待所有任务完成
anyOf:只要有一个任务完成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public  static  void  main (String[] args)  {    List<CompletableFuture> futures = Arrays.asList(CompletableFuture.completedFuture("hello" ),                                                     CompletableFuture.completedFuture(" world!" ),                                                     CompletableFuture.completedFuture(" hello" ),                                                     CompletableFuture.completedFuture("java!" ));     final  CompletableFuture<Void> allCompleted = CompletableFuture.allOf(futures.toArray(new  CompletableFuture []{}));     allCompleted.thenRun(() -> {         futures.stream().forEach(future -> {             try  {                 System.out.println("get future at:" +System.currentTimeMillis()+", result:" +future.get());             } catch  (InterruptedException | ExecutionException e) {                 e.printStackTrace();             }         });     }); } 
测试结果:
1 2 3 4 get future at:1568892339473, result:hello get future at:1568892339473, result: world! get future at:1568892339473, result: hello get future at:1568892339473, result:java! 
几乎同时完成任务!
2.7. 优化商品详情页 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 @Service public  class  ItemService  {    @Autowired      private  GmallPmsClient pmsClient;     @Autowired      private  GmallWmsClient wmsClient;     @Autowired      private  GmallSmsClient smsClient;     @Autowired      private  ThreadPoolExecutor threadPoolExecutor;     public  ItemVo load (Long skuId)  {         ItemVo  itemVo  =  new  ItemVo ();                  CompletableFuture<SkuEntity> skuCompletableFuture = CompletableFuture.supplyAsync(() -> {             ResponseVo<SkuEntity> skuEntityResponseVo = this .pmsClient.querySkuById(skuId);             SkuEntity  skuEntity  =  skuEntityResponseVo.getData();             if  (skuEntity == null ) {                 return  null ;             }             itemVo.setSkuId(skuId);             itemVo.setTitle(skuEntity.getTitle());             itemVo.setSubTitle(skuEntity.getSubtitle());             itemVo.setPrice(skuEntity.getPrice());             itemVo.setWeight(skuEntity.getWeight());             itemVo.setDefaltImage(skuEntity.getDefaultImage());             return  skuEntity;         }, threadPoolExecutor);                  CompletableFuture<Void> categoryCompletableFuture = skuCompletableFuture.thenAcceptAsync(skuEntity -> {             ResponseVo<List<CategoryEntity>> categoryResponseVo = this .pmsClient.queryCategoriesByCid3(skuEntity.getCategoryId());             List<CategoryEntity> categoryEntities = categoryResponseVo.getData();             itemVo.setCategories(categoryEntities);         }, threadPoolExecutor);                  CompletableFuture<Void> brandCompletableFuture = skuCompletableFuture.thenAcceptAsync(skuEntity -> {             ResponseVo<BrandEntity> brandEntityResponseVo = this .pmsClient.queryBrandById(skuEntity.getBrandId());             BrandEntity  brandEntity  =  brandEntityResponseVo.getData();             if  (brandEntity != null ) {                 itemVo.setBrandId(brandEntity.getId());                 itemVo.setBrandName(brandEntity.getName());             }         }, threadPoolExecutor);                  CompletableFuture<Void> spuCompletableFuture = skuCompletableFuture.thenAcceptAsync(skuEntity -> {             ResponseVo<SpuEntity> spuEntityResponseVo = this .pmsClient.querySpuById(skuEntity.getSpuId());             SpuEntity  spuEntity  =  spuEntityResponseVo.getData();             if  (spuEntity != null ) {                 itemVo.setSpuId(spuEntity.getId());                 itemVo.setSpuName(spuEntity.getName());             }         }, threadPoolExecutor);                  CompletableFuture<Void> skuImagesCompletableFuture = CompletableFuture.runAsync(() -> {             ResponseVo<List<SkuImagesEntity>> skuImagesResponseVo = this .pmsClient.queryImagesBySkuId(skuId);             List<SkuImagesEntity> skuImagesEntities = skuImagesResponseVo.getData();             itemVo.setImages(skuImagesEntities);         }, threadPoolExecutor);                  CompletableFuture<Void> salesCompletableFuture = CompletableFuture.runAsync(() -> {             ResponseVo<List<ItemSaleVo>> salesResponseVo = this .smsClient.querySalesBySkuId(skuId);             List<ItemSaleVo> sales = salesResponseVo.getData();             itemVo.setSales(sales);         }, threadPoolExecutor);                  CompletableFuture<Void> storeCompletableFuture = CompletableFuture.runAsync(() -> {             ResponseVo<List<WareSkuEntity>> wareSkuResponseVo = this .wmsClient.queryWareSkusBySkuId(skuId);             List<WareSkuEntity> wareSkuEntities = wareSkuResponseVo.getData();             if  (!CollectionUtils.isEmpty(wareSkuEntities)) {                 itemVo.setStore(wareSkuEntities.stream().anyMatch(wareSkuEntity -> wareSkuEntity.getStock() - wareSkuEntity.getStockLocked() > 0 ));             }         }, threadPoolExecutor);                  CompletableFuture<Void> saleAttrsCompletableFuture = skuCompletableFuture.thenAcceptAsync(skuEntity -> {             ResponseVo<List<SaleAttrValueVo>> saleAttrValueVoResponseVo = this .pmsClient.querySkuAttrValuesBySpuId(skuEntity.getSpuId());             List<SaleAttrValueVo> saleAttrValueVos = saleAttrValueVoResponseVo.getData();             itemVo.setSaleAttrs(saleAttrValueVos);         }, threadPoolExecutor);                  CompletableFuture<Void> saleAttrCompletableFuture = CompletableFuture.runAsync(() -> {             ResponseVo<List<SkuAttrValueEntity>> saleAttrResponseVo = this .pmsClient.querySkuAttrValuesBySkuId(skuId);             List<SkuAttrValueEntity> skuAttrValueEntities = saleAttrResponseVo.getData();             Map<Long, String> map = skuAttrValueEntities.stream().collect(Collectors.toMap(SkuAttrValueEntity::getAttrId, SkuAttrValueEntity::getAttrValue));             itemVo.setSaleAttr(map);         }, threadPoolExecutor);                  CompletableFuture<Void> skusJsonCompletableFuture = skuCompletableFuture.thenAcceptAsync(skuEntity -> {             ResponseVo<String> skusJsonResponseVo = this .pmsClient.querySkusJsonBySpuId(skuEntity.getSpuId());             String  skusJson  =  skusJsonResponseVo.getData();             itemVo.setSkusJson(skusJson);         }, threadPoolExecutor);                  CompletableFuture<Void> spuImagesCompletableFuture = skuCompletableFuture.thenAcceptAsync(skuEntity -> {             ResponseVo<SpuDescEntity> spuDescEntityResponseVo = this .pmsClient.querySpuDescById(skuEntity.getSpuId());             SpuDescEntity  spuDescEntity  =  spuDescEntityResponseVo.getData();             if  (spuDescEntity != null  && StringUtils.isNotBlank(spuDescEntity.getDecript())) {                 String[] images = StringUtils.split(spuDescEntity.getDecript(), "," );                 itemVo.setSpuImages(Arrays.asList(images));             }         }, threadPoolExecutor);                  CompletableFuture<Void> groupCompletableFuture = skuCompletableFuture.thenAcceptAsync(skuEntity -> {             ResponseVo<List<ItemGroupVo>> groupResponseVo = this .pmsClient.queryGoupsWithAttrValues(skuEntity.getCategoryId(), skuEntity.getSpuId(), skuId);             List<ItemGroupVo> itemGroupVos = groupResponseVo.getData();             itemVo.setGroups(itemGroupVos);         }, threadPoolExecutor);         CompletableFuture.allOf(categoryCompletableFuture, brandCompletableFuture, spuCompletableFuture,                 skuImagesCompletableFuture, salesCompletableFuture, storeCompletableFuture, saleAttrsCompletableFuture,                 saleAttrCompletableFuture, skusJsonCompletableFuture, spuImagesCompletableFuture, groupCompletableFuture).join();         return  itemVo;     } } 
3. 前后端联调 把课前资料《前端工程\动态页面》的对应item.html和common目录copy到工程:
 
并在pom.xml中引入thymeleaf依赖:
1 2 3 4 <dependency >     <groupId > org.springframework.boot</groupId >      <artifactId > spring-boot-starter-thymeleaf</artifactId >  </dependency > 
在application.yml中添加thymeleaf的配置:
修改nginx中的配置,添加item.gmall.com域名
网关中添加添加同步路由:
3.1. 跳转到商品详情页 修改ItemController的方法调转到商品详情页面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @RestController public  class  ItemController  {    @Autowired      private  ItemService itemService;     @GetMapping("{skuId}.html")      public  String load (@PathVariable("skuId") Long skuId, Model model) {         ItemVo  itemVo  =  this .itemService.load(skuId);         model.addAttribute("itemVo" , itemVo);         return  "item" ;     } } 
3.2. 页面结构 参照京东的商品详情页,如下:
主要包含3部分:面包屑、sku大图片及基本信息、页面下方的商品详情。
谷粒商城也是包含这三部分内容,页面源码结构如下:
3.3. 面包屑 1 2 3 4 5 6 7 8 9 10 11 12 <div  class ="crumb-wrap" >     <ul  class ="sui-breadcrumb" >          <li  th:each ="category : *{categories}" >              <a  href ="#"  th:text ="${category.name}" > 手机、数码、通讯</a >          </li >          <li >              <a  href ="#"  th:text ="*{brandName}" > Apple苹果</a >          </li >          <li  class ="active"  th:text ="*{spuName}" > iphone 6S系类</li >      </ul >  </div > 
3.4. 商品基本信息 
3.4.1. 商品图片列表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <div  class ="fl preview-wrap" >          <div  class ="zoom" >                   <div  id ="preview"  class ="spec-preview" >              <span  class ="jqzoom" >                  <img  th:jqimg ="*{defaltImage}"  th:src ="*{defaltImage}"  width ="400"  height ="400"  />              </span >          </div >                   <div  class ="spec-scroll" >              <a  class ="prev" > < </a >                           <div  class ="items" >                  <ul >                      <li  th:each ="image : *{images}" >                          <img  th:src ="${image.url}"  th:bimg ="${image.url}"  onmousemove ="preview(this)"  />                      </li >                  </ul >              </div >              <a  class ="next" > > </a >          </div >      </div >  </div > 
3.4.2. sku基本信息 包含:标题 副标题 价格 促销 重量等等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 <div  class ="sku-name" >     <h4  th:text ="*{title}" > Apple iPhone 6s(A1700)64G玫瑰金色 移动通信电信4G手机</h4 >  </div > <div  class ="news" >     <span  th:text ="*{subTitle}" > 推荐选择下方[移动优惠购],手机套餐齐搞定,不用换号,每月还有花费返</span >  </div > <div  class ="summary" >     <div  class ="summary-wrap" >          <div  class ="fl title" >              <i > 价  格</i >          </div >          <div  class ="fl price" >              <i > ¥</i >                           <em  th:text ="*{#numbers.formatDecimal(price, 1, 'COMMA', 2, 'POINT')}" > 5999.00</em >              <span > 降价通知</span >          </div >          <div  class ="fr remark" >              <i > 累计评价</i >              <em > 62344</em >          </div >      </div >      <div  class ="summary-wrap" >          <div  class ="fl title" >              <i > 促  销</i >          </div >          <div  class ="fl fix-width" >              <div  th:each ="sale : *{sales}" >                  <i  class ="red-bg"  th:text ="${sale.type}" > 加价购</i >                  <em  class ="t-gray"  th:text ="${sale.desc}" > 满999.00另加20.00元,或满1999.00另加30.00元,或满2999.00另加40.00元,即可在购物车换购热销商品</em >              </div >          </div >      </div >  </div > <div  class ="support" >     <div  class ="summary-wrap" >          <div  class ="fl title" >              <i > 支  持</i >          </div >          <div  class ="fl fix-width" >              <em  class ="t-gray" > 以旧换新,闲置手机回收 4G套餐超值抢 礼品购</em >          </div >      </div >      <div  class ="summary-wrap" >          <div  class ="fl title" >              <i > 配 送 至</i >          </div >          <div  class ="fl fix-width" >              <em  class ="t-gray" > 上海市松江区大江商厦6层</em >          </div >      </div >      <div  class ="summary-wrap" >          <div  class ="fl title" >              <i > 重 量(g)</i >          </div >          <div  class ="fl fix-width" >              <em  class ="t-gray"  th:text ="*{weight}" > 500</em >          </div >      </div >  </div > 
3.4.3. 销售属性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <div  class ="clearfix choose"  id ="app" >     <div  id ="specification"  class ="summary-wrap clearfix" >          <dl  v-for ="attr in saleAttrs"  :key ="attr.attrId" >              <dt >                  <div  class ="fl title" >                      <i > 选择{{attr.attrName}}</i >                  </div >              </dt >              <dd  v-for ="attrValue,index in attr.attrValues"  :key ="index"  @click ="saleAttr[attr.attrId]=attrValue" >                  <a  href ="javascript:;"  :class ="{'selected':attrValue === saleAttr[attr.attrId]}" > {{attrValue}}</a >              </dd >          </dl >      </div >  
vuejs的代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <script  th:inline ="javascript" >     new  Vue ({         el : '#app' ,         data : {             saleAttrs : [[${itemVo.saleAttrs }]],              saleAttr : [[${itemVo.saleAttr }]],             skusJson : JSON .parse (decodeURI ([[${itemVo.skusJson }]])),          skuId : [[${itemVo.skuId }]]     },         watch : {             saleAttr : {                 deep : true ,                     handler (newSaleAttr ) {                     console .log (newSaleAttr)                     let  key = Object .values (newSaleAttr).join ("," )                     this .skuId  = this .skusJson [key]                     window .location  = `http://item.gmall.com/${this .skuId} .html`                  }             }         }     }) </script > 
3.5. 商品详情 商品描述大海报的渲染:
规格参数的渲染:
1 2 3 4 5 6 7 8 9 10 11 12 13 <div  id ="two"  class ="tab-pane" >     <div  class ="Ptable" >          <div  class ="Ptable-item"  th:each ="group : *{groups}" >              <h3  th:text ="${group.groupName}" > 主体</h3 >              <dl >                  <div  th:each ="attr : ${group.attrValues}" >                      <dt  th:text ="${attr.attrName}" > 品牌</dt > <dd  th:text ="${attr.attrValue}" > 华为(HUAWEI)</dd >                  </div >              </dl >          </div >      </div >  </div >