1. 商品详情

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

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

1.1. 创建module

1568530059417

1586073353159

1586075917607

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

}

别忘了配置网关:

1586076078719

1.2. 数据模型

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

以京东为例:

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

1589709971270

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

面包屑信息:

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

sku相关信息:

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

spu下所有销售组合:

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

商品详情信息:

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

最终设计如下:

1589710328081

商品详情页总的数据模型: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;

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

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。为渲染商品详情页,需要的远程数据结构提供数据:

  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下的所有销售属性

    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: ['6G', '8G', '12G']
    },
    {
    attrId: 9,
    attrName: '存储',
    attrValues: ['128G', '256G', '512G']
    }
    ]
  9. 根据skuId查询当前sku的销售属性

  10. 根据sku中的spuId查询spu下所有sku:销售属性组合与skuId映射关系

    1
    {'白色,8G,128G': 4, '白色,8G,256G': 5, '白色,8G,512G': 6, '白色,12G,128G': 7}
  11. 根据sku中spuId查询spu的描述信息(已有)

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

1.3.1. 添加远程接口

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

1586079205681

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
/**
* 根据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中添加方法:

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);
// 以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接口添加方法:

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) {
// [{"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方法:

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) {

// 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数据

1589713495784

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

// 根据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
2
3
4
5
6
7
8
9
10
11
12
13
// 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:通过如下两种方式初始化线程池:

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

1568552614487

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");
//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. 多任务组合

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

// 根据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到工程:

1589448529092

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

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

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

1589450178655

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

1589448463053

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

1589449144103

3.1. 跳转到商品详情页

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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. 页面结构

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

1589461080401

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

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

1589460868869

3.3. 面包屑

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 面包屑:分类 品牌 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. 商品基本信息

1589714713684

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">&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基本信息

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

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>
<!--
${#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. 销售属性

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的关系
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. 商品详情

商品描述大海报的渲染:

1589715253381

规格参数的渲染:

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>