1. 商品详情

当用户搜索到商品,肯定会点击查看,就会进入商品详情页,接下来我们完成商品详情页的展示。

商品详情浏览量比较大,并发高,我们会独立开启一个微服务,用来展示商品详情。

1.1. 创建module

10.商品详情页与异步编排 - 图1

10.商品详情页与异步编排 - 图2

10.商品详情页与异步编排 - 图3

pom.xml依赖:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  4. <modelVersion>4.0.0</modelVersion>
  5. <parent>
  6. <groupId>com.atguigu</groupId>
  7. <artifactId>gmall</artifactId>
  8. <version>0.0.1-SNAPSHOT</version>
  9. </parent>
  10. <groupId>com.atguigu</groupId>
  11. <artifactId>gmall-item</artifactId>
  12. <version>0.0.1-SNAPSHOT</version>
  13. <name>gmall-item</name>
  14. <description>谷粒商城商品详情页</description>
  15. <properties>
  16. <java.version>1.8</java.version>
  17. </properties>
  18. <dependencies>
  19. <dependency>
  20. <groupId>com.atguigu</groupId>
  21. <artifactId>gmall-pms-interface</artifactId>
  22. <version>0.0.1-SNAPSHOT</version>
  23. </dependency>
  24. <dependency>
  25. <groupId>com.atguigu</groupId>
  26. <artifactId>gmall-wms-interface</artifactId>
  27. <version>0.0.1-SNAPSHOT</version>
  28. </dependency>
  29. <dependency>
  30. <groupId>com.atguigu</groupId>
  31. <artifactId>gmall-sms-interface</artifactId>
  32. <version>0.0.1-SNAPSHOT</version>
  33. </dependency>
  34. <dependency>
  35. <groupId>org.springframework.boot</groupId>
  36. <artifactId>spring-boot-starter-web</artifactId>
  37. </dependency>
  38. <dependency>
  39. <groupId>com.alibaba.cloud</groupId>
  40. <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
  41. </dependency>
  42. <dependency>
  43. <groupId>com.alibaba.cloud</groupId>
  44. <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  45. </dependency>
  46. <dependency>
  47. <groupId>com.alibaba.cloud</groupId>
  48. <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
  49. </dependency>
  50. <dependency>
  51. <groupId>org.springframework.cloud</groupId>
  52. <artifactId>spring-cloud-starter-openfeign</artifactId>
  53. </dependency>
  54. <dependency>
  55. <groupId>org.springframework.cloud</groupId>
  56. <artifactId>spring-cloud-starter-zipkin</artifactId>
  57. </dependency>
  58. <dependency>
  59. <groupId>org.springframework.boot</groupId>
  60. <artifactId>spring-boot-starter-test</artifactId>
  61. <scope>test</scope>
  62. <exclusions>
  63. <exclusion>
  64. <groupId>org.junit.vintage</groupId>
  65. <artifactId>junit-vintage-engine</artifactId>
  66. </exclusion>
  67. </exclusions>
  68. </dependency>
  69. </dependencies>
  70. <build>
  71. <plugins>
  72. <plugin>
  73. <groupId>org.springframework.boot</groupId>
  74. <artifactId>spring-boot-maven-plugin</artifactId>
  75. </plugin>
  76. </plugins>
  77. </build>
  78. </project>

bootstrap.properties:

spring:
  application:
    name: item-service
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848

application.properties:

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:

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class GmallItemApplication {

    public static void main(String[] args) {
        SpringApplication.run(GmallItemApplication.class, args);
    }

}

别忘了配置网关:

10.商品详情页与异步编排 - 图4

1.2. 数据模型

当点击搜索列表中的一个记录,会跳转到商品详情页。这个商品详情页是一个spu?还是sku

以京东为例:

在京东搜索小米,出现搜索列表后。点击其中一条记录,跳转到商品详情页,这个商品详情页展示的是:

10.商品详情页与异步编排 - 图5

结合页面,商品详情页需要的数据有:

面包屑信息:

  • 三级分类
  • 品牌
  • spu的名称

sku相关信息:

  • sku的基本信息(标题、副标题、价格、大图片等)
  • sku的所有图片
  • sku的所有促销信息
  • sku的库存情况(是否有货)

spu下所有销售组合:

  • 每个销售属性可取值集合,方便渲染可选值列表
  • 当前商品的销售属性,方便渲染选中项
  • 销售属性组合与skuId的映射关系,方便切换sku

商品详情信息:

  • spu的描述信息
  • spu的所有基本规格分组及规格参数

最终设计如下:

10.商品详情页与异步编排 - 图6

商品详情页总的数据模型:ItemVO

@Data
public class ItemVo {

    // 三级分类
    private List<CategoryEntity> categories;

    // 品牌
    private Long brandId;
    private String brandName;

    // spu
    private Long spuId;
    private String spuName;

    // sku
    private Long skuId;
    private String title;
    private String subTitle;
    private BigDecimal price;
    private Integer weight;
    private String defaltImage;

    // sku图片
    private List<SkuImagesEntity> images;

    // 营销信息
    private List<ItemSaleVo> sales;

    // 是否有货
    private Boolean store = false;

    // sku所属spu下的所有sku的销售属性
    // [{attrId: 3, attrName: '颜色', attrValues: '白色','黑色','粉色'},
    // {attrId: 8, attrName: '内存', attrValues: '6G','8G','12G'},
    // {attrId: 9, attrName: '存储', attrValues: '128G','256G','512G'}]
    private List<SaleAttrValueVo> saleAttrs;

    // 当前sku的销售属性:{3:'白色',8:'8G',9:'128G'}
    private Map<Long, String> saleAttr;

    // sku列表:{'白色,8G,128G': 4, '白色,8G,256G': 5, '白色,8G,512G': 6, '白色,12G,128G': 7}
    private String skusJson;

    // spu的海报信息
    private List<String> spuImages;

    // 规格参数组及组下的规格参数(带值)
    private List<ItemGroupVo> groups;
}

ItemSaleVo:

@Data
public class ItemSaleVo {
    private String type; // 积分 满减 打折
    private String desc; // 描述信息
}

ItemGroupVo:

@Data
public class ItemGroupVo {

    private String groupName;
    private List<AttrValueVo> attrValues;
}

AttrValueVo:

@Data
public class AttrValueVo {

    private Long attrId;
    private String attrName;
    private String attrValue;
}

SaleAttrValueVo:

@Data
public class SaleAttrValueVo {

    private Long attrId;
    private String attrName;
    private Set<String> attrValues;
}

1.3. 远程调用接口

跳转到商品详情页时,已知条件只有一个skuId。为渲染商品详情页,需要的远程数据结构提供数据:

  1. 根据skuId查询sku(已有)
  2. 根据sku中的三级分类id查询一二三级分类
  3. 根据sku中的品牌id查询品牌(已有)
  4. 根据sku中的spuId查询spu信息(已有)
  5. 根据skuId查询sku所有图片
  6. 根据skuId查询sku的所有营销信息
  7. 根据skuId查询sku的库存信息(已有)
  8. 根据sku中的spuId查询spu下的所有销售属性json [ { attrId: 3, attrName: '颜色', attrValues: ['白色', '黑色', '粉色'] }, { attrId: 8, attrName: '内存', attrValues: ['6G', '8G', '12G'] }, { attrId: 9, attrName: '存储', attrValues: ['128G', '256G', '512G'] } ]

  9. 根据skuId查询当前sku的销售属性

  10. 根据sku中的spuId查询spu下所有sku:销售属性组合与skuId映射关系json {'白色,8G,128G': 4, '白色,8G,256G': 5, '白色,8G,512G': 6, '白色,12G,128G': 7}

  11. 根据sku中spuId查询spu的描述信息(已有)

  12. 根据分类id、spuId及skuId查询分组及组下的规格参数值

1.3.1. 添加远程接口

这些数据模型需要调用远程接口从其他微服务获取,所以这里先编写feign接口

10.商品详情页与异步编排 - 图7

GmallPmsClient:

@FeignClient("pms-service")
public interface GmallPmsClient extends GmallPmsApi {
}

GmallSmsClient:

@FeignClient("sms-service")
public interface GmallSmsClient extends GmallSmsApi {
}

GmallWmsClient:

@FeignClient("wms-service")
public interface GmallWmsClient extends GmallWmsApi {
}

有些接口已经有了,直接给GmallPmsApi添加方法即可:

/**
     * 根据id查询sku信息
     * @param id
     * @return
     */
@GetMapping("pms/sku/{id}")
public ResponseVo<SkuEntity> querySkuById(@PathVariable("id") Long id);

/**
     * 根据id查询spu信息
     * @param id
     * @return
     */
@GetMapping("pms/spu/{id}")
public ResponseVo<SpuEntity> querySpuById(@PathVariable("id") Long id);

/**
     * 根据id查询spu的描述信息
     * @param spuId
     * @return
     */
@GetMapping("pms/spudesc/{spuId}")
public ResponseVo<SpuDescEntity> querySpuDescById(@PathVariable("spuId") Long spuId);

1.3.2. 根据三级分类id查询一二三级分类

在CategoryController中添加方法:

@GetMapping("all/{cid3}")
public ResponseVo<List<CategoryEntity>> queryCategoriesByCid3(@PathVariable("cid3")Long cid3){
    List<CategoryEntity> itemCategoryVos = this.categoryService.queryCategoriesByCid3(cid3);
    return ResponseVo.ok(itemCategoryVos);
}

在CategoryService接口中添加方法:

List<CategoryEntity> queryCategoriesByCid3(Long cid3);

在CategoryServiceImpl实现类中添加方法:

@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中添加接口方法:

@GetMapping("pms/category/all/{cid3}")
public ResponseVo<List<CategoryEntity>> queryCategoriesByCid3(@PathVariable("cid3")Long cid3);

1.3.3. 根据skuId查询sku的图片

在SkuImagesController中添加:

@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中添加接口方法:

@GetMapping("pms/skuimages/sku/{skuId}")
public ResponseVo<List<SkuImagesEntity>> queryImagesBySkuId(@PathVariable("skuId")Long skuId);

1.3.4. 查询sku的营销信息

在SkuBoundsController中添加查询营销信息的方法:

@GetMapping("sku/{skuId}")
public ResponseVo<List<ItemSaleVo>> querySalesBySkuId(@PathVariable("skuId")Long skuId){
    List<ItemSaleVo> itemSaleVos = this.skuBoundsService.querySalesBySkuId(skuId);
    return ResponseVo.ok(itemSaleVos);
}

在SkuBoundsService中添加接口方法:

List<ItemSaleVo> querySalesBySkuId(Long skuId);

在SkuBoundsServiceImpl中实现接口方法:

@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中添加数据接口方法

@GetMapping("sms/skubounds/sku/{skuId}")
public ResponseVo<List<ItemSaleVo>> querySalesBySkuId(@PathVariable("skuId")Long skuId);

1.3.5. 根据spuId查询spu下的所有销售属性

SkuAttrValueController中添加查询方法:

@GetMapping("spu/{spuId}")
public ResponseVo<List<SaleAttrValueVo>> querySkuAttrValuesBySpuId(@PathVariable("spuId")Long spuId){
    List<SaleAttrValueVo> saleAttrValueVos = this.skuAttrValueService.querySkuAttrValuesBySpuId(spuId);
    return ResponseVo.ok(saleAttrValueVos);
}

在SkuSaleAttrValueService接口中添加抽象方法:

List<SaleAttrValueVo> querySkuAttrValuesBySpuId(Long spuId);

在SkuSaleAttrValueServiceImpl中实现该方法:

@Autowired
private SkuAttrValueMapper skuAttrValueMapper;

@Override
public List<SaleAttrValueVo> querySkuAttrValuesBySpuId(Long spuId) {
    List<AttrValueVo> attrValueVos = skuAttrValueMapper.querySkuAttrValuesBySpuId(spuId);
    // 以attrId进行分组
    Map<Long, List<AttrValueVo>> map = attrValueVos.stream().collect(groupingBy(AttrValueVo::getAttrId));

    // 创建一个List<SaleAttrValueVo>
    List<SaleAttrValueVo> saleAttrValueVos = new ArrayList<>();
    map.forEach((attrId, attrs) -> {
        SaleAttrValueVo saleAttrValueVo = new SaleAttrValueVo();
        // attrId
        saleAttrValueVo.setAttrId(attrId);
        // attrName
        saleAttrValueVo.setAttrName(attrs.get(0).getAttrName());
        // attrValues
        Set<String> attrValues = attrs.stream().map(AttrValueVo::getAttrValue).collect(Collectors.toSet());
        saleAttrValueVo.setAttrValues(attrValues);
        saleAttrValueVos.add(saleAttrValueVo);
    });

    return saleAttrValueVos;
}

SkuAttrValueMapper接口添加方法:

@Mapper
public interface SkuAttrValueMapper extends BaseMapper<SkuAttrValueEntity> {

    List<AttrValueVo> querySkuAttrValuesBySpuId(Long spuId);
}

SkuAttrValueMapper.xml中配置映射:

<?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中添加数据接口:

@GetMapping("pms/skuattrvalue/spu/{spuId}")
public ResponseVo<List<SaleAttrValueVo>> querySkuAttrValuesBySpuId(@PathVariable("spuId")Long spuId);

1.3.6. 查询spu下skuId及销售属性的关系

给SkuAttrValueController添加方法:

@GetMapping("spu/sku/{spuId}")
public ResponseVo<String> querySkusJsonBySpuId(@PathVariable("spuId") Long spuId){
    String skusJson = this.skuAttrValueService.querySkusJsonBySpuId(spuId);
    return ResponseVo.ok(skusJson);
}

给SkuAttrValueService接口添加方法:

String querySkusJsonBySpuId(Long spuId);

SkuAttrValueServiceImpl实现类实现接口方法:

@Override
public String querySkusJsonBySpuId(Long spuId) {
    // [{"sku_id": 3, "attr_values": "暗夜黑,12G,512G"}, {"sku_id": 4, "attr_values": "白天白,12G,512G"}]
    List<Map<String, Object>> skus = this.skuAttrValueMapper.querySkusJsonBySpuId(spuId);
    // 转换成:{'暗夜黑,12G,512G': 3, '白天白,12G,512G': 4}
    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方法:

@Mapper
public interface SkuAttrValueMapper extends BaseMapper<SkuAttrValueEntity> {

    List<AttrValueVo> querySkuAttrValuesBySpuId(Long spuId);

    List<Map<String, Object>> querySkusJsonBySpuId(Long spuId);
}

给SkuAttrValueMapper.xml映射文件添加映射:

<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添加接口方法:

@GetMapping("pms/skuattrvalue/spu/sku/{spuId}")
public ResponseVo<String> querySkusJsonBySpuId(@PathVariable("spuId") Long spuId);

1.3.7. 查询组及组下参数和值

在AttrGroupController中添加方法:

@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接口中添加接口方法:

List<ItemGroupVo> queryGroupsBySpuIdAndCid(Long spuId, Long skuId, Long cid);

在AttrGroupServiceImpl实现类中添加实现方法:

@Autowired
private SpuAttrValueMapper spuAttrValueMapper;

@Autowired
private SkuAttrValueMapper skuAttrValueMapper;

@Override
public List<ItemGroupVo> queryGroupsBySpuIdAndCid(Long spuId, Long skuId, Long cid) {

    // 1.根据cid查询分组
    List<AttrGroupEntity> attrGroupEntities = this.list(new QueryWrapper<AttrGroupEntity>().eq("category_id", cid));

    if (CollectionUtils.isEmpty(attrGroupEntities)){
        return null;
    }

    // 2.遍历分组查询每个组下的attr
    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());
            // 3.attrId结合spuId查询规格参数对应值
            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数据

10.商品详情页与异步编排 - 图8

ItemController:

@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:

@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();

        // 根据skuId查询sku的信息1
        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());

        // 根据cid3查询分类信息2
        ResponseVo<List<CategoryEntity>> categoryResponseVo = this.pmsClient.queryCategoriesByCid3(skuEntity.getCategoryId());
        List<CategoryEntity> categoryEntities = categoryResponseVo.getData();
        itemVo.setCategories(categoryEntities);

        // 根据品牌的id查询品牌3
        ResponseVo<BrandEntity> brandEntityResponseVo = this.pmsClient.queryBrandById(skuEntity.getBrandId());
        BrandEntity brandEntity = brandEntityResponseVo.getData();
        if (brandEntity != null) {
            itemVo.setBrandId(brandEntity.getId());
            itemVo.setBrandName(brandEntity.getName());
        }

        // 根据spuId查询spu4
        ResponseVo<SpuEntity> spuEntityResponseVo = this.pmsClient.querySpuById(skuEntity.getSpuId());
        SpuEntity spuEntity = spuEntityResponseVo.getData();
        if (spuEntity != null) {
            itemVo.setSpuId(spuEntity.getId());
            itemVo.setSpuName(spuEntity.getName());
        }

        // 跟据skuId查询图片5
        ResponseVo<List<SkuImagesEntity>> skuImagesResponseVo = this.pmsClient.queryImagesBySkuId(skuId);
        List<SkuImagesEntity> skuImagesEntities = skuImagesResponseVo.getData();
        itemVo.setImages(skuImagesEntities);

        // 根据skuId查询sku营销信息6
        ResponseVo<List<ItemSaleVo>> salesResponseVo = this.smsClient.querySalesBySkuId(skuId);
        List<ItemSaleVo> sales = salesResponseVo.getData();
        itemVo.setSales(sales);

        // 根据skuId查询sku的库存信息7
        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));
        }

        // 根据spuId查询spu下的所有sku的销售属性8
        ResponseVo<List<SaleAttrValueVo>> saleAttrValueVoResponseVo = this.pmsClient.querySkuAttrValuesBySpuId(skuEntity.getSpuId());
        List<SaleAttrValueVo> saleAttrValueVos = saleAttrValueVoResponseVo.getData();
        itemVo.setSaleAttrs(saleAttrValueVos);

        // 当前sku的销售属性9
        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);

        // 根据spuId查询spu下的所有sku及销售属性的映射关系10
        ResponseVo<String> skusJsonResponseVo = this.pmsClient.querySkusJsonBySpuId(skuEntity.getSpuId());
        String skusJson = skusJsonResponseVo.getData();
        itemVo.setSkusJson(skusJson);

        // 根据spuId查询spu的海报信息11
        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));
        }

        // 根据cid3 spuId skuId查询组及组下的规格参数及值 12
        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. 获取sku的基本信息    0.5s

// 2. 获取sku的图片信息    0.5s

// 3. 获取sku的促销信息 TODO 1s

// 4. 获取spu的所有销售属性    1s

// 5. 获取规格参数组及组下的规格参数 TODO    1.5s

// 6. spu详情 TODO    1s

.........

那么,用户需要6.5s后才能看到商品详情页的内容。很显然是不能接受的。

如果有多个线程同时完成这6步操作,也许只需要1.5s即可完成响应。

2.1. 线程回顾

初始化线程的4种方式:

  1. 继承Thread
  2. 实现Runnable接口
  3. 实现Callable接口 + FutureTask (可以拿到返回结果,可以处理异常)
  4. 线程池

方式1和方式2:主进程无法获取线程的运算结果。不适合当前场景

方式3:主进程可以获取线程的运算结果,并设置给itemVO,但是不利于控制服务器中的线程资源。可以导致服务器资源耗尽。

方式4:通过如下两种方式初始化线程池:

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接口的实现类,都可以获取线程的执行结果。

10.商品详情页与异步编排 - 图9

2.3. 创建异步对象

CompletableFuture 提供了四个静态方法来创建一个异步操作。

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。主要是下面的方法:

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可能会使用其他线程执行(如果是使用相同的线程池,也可能会被同一个线程选中执行)

代码示例:

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默认是异步执行的。这里所谓的异步指的是不在当前线程内执行。

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:当前任务的返回值类型

代码演示:

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");
            //int i = 10 / 0;
            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. 多任务组合

public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs);

public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs);

allOf:等待所有任务完成

anyOf:只要有一个任务完成

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();
            }
        });
    });
}

测试结果:

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. 优化商品详情页

@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();

        // 根据skuId查询sku的信息1
        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);

        // 根据cid3查询分类信息2
        CompletableFuture<Void> categoryCompletableFuture = skuCompletableFuture.thenAcceptAsync(skuEntity -> {
            ResponseVo<List<CategoryEntity>> categoryResponseVo = this.pmsClient.queryCategoriesByCid3(skuEntity.getCategoryId());
            List<CategoryEntity> categoryEntities = categoryResponseVo.getData();
            itemVo.setCategories(categoryEntities);
        }, threadPoolExecutor);

        // 根据品牌的id查询品牌3
        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);

        // 根据spuId查询spu4
        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);

        // 跟据skuId查询图片5
        CompletableFuture<Void> skuImagesCompletableFuture = CompletableFuture.runAsync(() -> {
            ResponseVo<List<SkuImagesEntity>> skuImagesResponseVo = this.pmsClient.queryImagesBySkuId(skuId);
            List<SkuImagesEntity> skuImagesEntities = skuImagesResponseVo.getData();
            itemVo.setImages(skuImagesEntities);
        }, threadPoolExecutor);

        // 根据skuId查询sku营销信息6
        CompletableFuture<Void> salesCompletableFuture = CompletableFuture.runAsync(() -> {
            ResponseVo<List<ItemSaleVo>> salesResponseVo = this.smsClient.querySalesBySkuId(skuId);
            List<ItemSaleVo> sales = salesResponseVo.getData();
            itemVo.setSales(sales);
        }, threadPoolExecutor);

        // 根据skuId查询sku的库存信息7
        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);

        // 根据spuId查询spu下的所有sku的销售属性
        CompletableFuture<Void> saleAttrsCompletableFuture = skuCompletableFuture.thenAcceptAsync(skuEntity -> {
            ResponseVo<List<SaleAttrValueVo>> saleAttrValueVoResponseVo = this.pmsClient.querySkuAttrValuesBySpuId(skuEntity.getSpuId());
            List<SaleAttrValueVo> saleAttrValueVos = saleAttrValueVoResponseVo.getData();
            itemVo.setSaleAttrs(saleAttrValueVos);
        }, threadPoolExecutor);

        // 当前sku的销售属性
        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);

        // 根据spuId查询spu下的所有sku及销售属性的映射关系
        CompletableFuture<Void> skusJsonCompletableFuture = skuCompletableFuture.thenAcceptAsync(skuEntity -> {
            ResponseVo<String> skusJsonResponseVo = this.pmsClient.querySkusJsonBySpuId(skuEntity.getSpuId());
            String skusJson = skusJsonResponseVo.getData();
            itemVo.setSkusJson(skusJson);
        }, threadPoolExecutor);

        // 根据spuId查询spu的海报信息9
        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);

        // 根据cid3 spuId skuId查询组及组下的规格参数及值 10
        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到工程:

10.商品详情页与异步编排 - 图10

并在pom.xml中引入thymeleaf依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

在application.yml中添加thymeleaf的配置:

10.商品详情页与异步编排 - 图11

修改nginx中的配置,添加item.gmall.com域名

10.商品详情页与异步编排 - 图12

网关中添加添加同步路由:

10.商品详情页与异步编排 - 图13

3.1. 跳转到商品详情页

修改ItemController的方法调转到商品详情页面:

@RestController
//@RequestMapping("item")
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. 页面结构

参照京东的商品详情页,如下:

10.商品详情页与异步编排 - 图14

主要包含3部分:面包屑、sku大图片及基本信息、页面下方的商品详情。

谷粒商城也是包含这三部分内容,页面源码结构如下:

10.商品详情页与异步编排 - 图15

3.3. 面包屑

<!-- 面包屑:分类 品牌 spu -->
<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. 商品基本信息

10.商品详情页与异步编排 - 图16

3.4.1. 商品图片列表

<!-- 商品大图片列表 -->
<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">&lt;</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">&gt;</a>
        </div>
    </div>
</div>

3.4.2. sku基本信息

包含:标题 副标题 价格 促销 重量等等

<!-- 标题 -->
<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>
            <!--
${#numbers.formatDecimal(num,0,'COMMA',2,'POINT')}则显示 .00
${#numbers.formatDecimal(num,1,'COMMA',2,'POINT')}则显示 0.00
-->
            <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. 销售属性

<!-- 销售属性 -->
<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的代码实现:

<script th:inline="javascript">
    new Vue({
        el: '#app',
        data: {
            saleAttrs: [[${itemVo.saleAttrs}]], // 销售属性可选项
            saleAttr: [[${itemVo.saleAttr}]],
            skusJson: JSON.parse(decodeURI([[${itemVo.skusJson}]])), // 销售属性和skuId的关系
        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. 商品详情

商品描述大海报的渲染:

10.商品详情页与异步编排 - 图17

规格参数的渲染:

<!-- 规格参数 -->
<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>