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 的区别: whenComplete:是执行当前任务的线程执行继续执行 whenComplete 的任务。 whenCompleteAsync:是执行把 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> T:上一个任务返回结果的类型 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 >