九、商城业务 | 商品上架

【摘要】商城业务 shop-business~

商城业务-商品上架-sku在es中存储模型分析

上架的商品才可以在网站展示。
上架的商品需要可以被检索。

商品Mapping

分析:商品上架在es中是否存sku还是spu?
(1).检索的时候输入名字,是需要按照sku的title进行全文检索的。
(2).检索使用商品规格,规格是spu的公共属性,每个spu是一样的。
(3).按照分类id进去的都是直接列出spu的,还可以切换。
(4).我们如果将spu的全量信息保存到es中(包括spu属性)就太多字段了。
(5).我们如果将spu以及其他包含的sku信息保存到es中,也可以方便检索。但是sku属于spu的级联对象,在es中需要nested模型,这种性能差点。
(6).但是储存和检索我们必须性能折中。
(7).如果我们分拆存储,spu和attr一个索引,sku单独一个索引可能涉及的问题
检索商品的名字,如”手机”,对应的spu有很多,我们要分析出这些spu的所有关联属性,再做一次查询,就必须将所有spu_id都发出去。假设有1万个数据,数据传输一次就10000*4=4MB; 并发情况下假设1000检索请求,那就是4GB的传输数据,传输阻塞时长会很长,业务更无法继续。
所以,我们如下设计,这样才时文档区别于关系型数据库的地方,宽表设计,不能去考虑数据库范式。

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
PUT product
{
"mappings": {
"properties": {
"skuId":{
"type": "long"
},
"spuId":{
"type": "keyword"
},
"skuTitle":{
"type": "text",
"analyzer":"ik_smart"
},
"skuPrice":{
"type": "keyword"
},
"skuImg":{
"type": "keyword",
"index": false,
"doc_values": false
},
"saleCount":{
"type": "long"
},
"hasStock":{
"type": "boolean"
},
"hotScore":{
"type": "long"
},
"brandId":{
"type": "long"
},
"catalogId":{
"type": "long"
},
"brandName":{
"type": "keyword",
"index": false,
"doc_values": false
},
"brandImg":{
"type": "keyword",
"index": false,
"doc_values": false
},
"catalogName":{
"type": "keyword",
"index": false,
"doc_values": false
},
"attrs":{
"type": "nested",
"properties": {
"attrId":{
"type": "long"
},
"attrName":{
"type": "keyword",
"index": false,
"doc_values": false
},
"attrValue":{
"type": "keyword"
}
}
}
}
}
}

商城业务-商品上架-nested数据类型场景

https://www.elastic.co/guide/en/elasticsearch/reference/current/nested.html

商城业务-商品上架-构造基本数据

创建数据传输对象SkuEsModel。这边的数据结构是根据所需要存在es中的数据结构定义的。这部分业务逻辑主要在

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
package com.aiz.common.to.es;

import lombok.Data;
import java.math.BigDecimal;
import java.util.List;

@Data
public class SkuEsModel {
private Long skuId;
private Long spuId;
private String skuTitle;
private BigDecimal skuPrice;
private String skuImg;
private Long saleCount;
private Boolean hasStock;
private Long hotScore;
private Long brandId;
private Long catalogId;
private String brandName;
private String brandImg;
private String catalogName;
private List<Attr> attrs;

@Data
public static class Attr{
private Long attrId;
private String attrName;
private String attrValue;
}
}

商城业务-商品上架-构造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
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
@Service("spuInfoService")
public class SpuInfoServiceImpl extends ServiceImpl<SpuInfoDao, SpuInfoEntity> implements SpuInfoService {
@Override
public void up(Long spuId) {
//1.查出当前spuId对应的所有sku信息,品牌的名字。
List<SkuInfoEntity> skus = skuInfoService.getSkusBySpuId(spuId);
List<Long> skuIdList = skus.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());
// TODO 4.查询当前sku的所有可以被用来检索的规格属性
List<ProductAttrValueEntity> baseAttrs = attrValueService.baseAttrlistforspu(spuId);
List<Long> attrIds = baseAttrs.stream().map(attr -> {
return attr.getAttrId();
}).collect(Collectors.toList());
List<Long> searchAttrIds = attrService.selectSearchAttrIds(attrIds);
Set<Long> idSet = new HashSet<>(searchAttrIds);

List<SkuEsModel.Attr> attrs = new ArrayList<>();
List<SkuEsModel.Attr> attrList = baseAttrs.stream().filter(item -> {
return idSet.contains(item.getAttrId());
}).map(item -> {
SkuEsModel.Attr attrs1 = new SkuEsModel.Attr();
BeanUtils.copyProperties(item, attrs1);
return attrs1;
}).collect(Collectors.toList());

// TODO 1.发送远程调用,库存系统查询是否有库存
Map<Long, Boolean> stockMap = null;
try{
R<List<SkuHasStockVo>> r = wareFeignService.getSkusHasStock(skuIdList);
//
TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>() {};
stockMap = r.getData(typeReference).stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, item -> item.getHasStock()));
}catch (Exception e){
log.error("库存服务查询异常:原因{}",e);
}

//2.封装每个sku的信息
Map<Long, Boolean> finalStockMap = stockMap;
List<SkuEsModel> upProducts = skus.stream().map(sku->{
//组装需要的数据
SkuEsModel esModel = new SkuEsModel();
BeanUtils.copyProperties(sku,esModel);
//skuPrice,skuImg,hasStock,hotScore,brandName,brandImg,catalogName,attrs[attrId,attrName,attrValue]
esModel.setSkuPrice(sku.getPrice());
esModel.setSkuImg(sku.getSkuDefaultImg());
//hasStock,hotScore
// TODO 1.发送远程调用,库存系统查询是否有库存
//设置库存信息
if(finalStockMap == null){
esModel.setHasStock(true);
}else{
esModel.setHasStock(finalStockMap.get(sku.getSkuId()));
}

// TODO 2.热度评分。0。
esModel.setHotScore(0L);
// TODO 3.查询品牌和分类的名字信息
BrandEntity brand = brandService.getById(esModel.getBrandId());
esModel.setBrandName(brand.getName());
esModel.setBrandImg(brand.getLogo());
CategoryEntity category = categoryService.getById(esModel.getCatalogId());
esModel.setCatalogName(category.getName());

// TODO 4.查询当前sku的所有可以被用来检索的规格属性
//设置检索属性
esModel.setAttrs(attrList);

return esModel;
}).collect(Collectors.toList());

//TODO 5.将数据发送给es进行保存:zheli-search
R r = searchFeignService.productStatusUp(upProducts);
if(r.getCode() == 0){
//远程调用成功
//TODO 6.修改当前spu的状态
baseMapper.updateSpuStatus(spuId, ProductConstant.StatusEnum.SPU_UP.getCode());
}else{
//远程调用失败
//TODO 7.重复调用?接口幂等性:重试机制?xx
/**
* Feign调用流程
* SynchronousMethodHandler
* 1.构造请求数据,将对象转为json;
* RequestTemplate template = buildTemplateFromArgs.create(argv);
* 2.发送请求进行执行(执行成功会解码响应数据);
* executeAndDecode(template)
* 3.执行请求会有重试机制;
* while(true){
* try{
* executeAndDecode(template);
* }catch(){
* try{retryer.continueOrPropagate(e);}catch(){throw ex;}
* continue;
* }
* }
*/
}
}

商城业务-商品上架-远程查询库存&泛型结果封装

商城业务-商品上架-远程上架接口

商城业务-商品上架-上架接口调试&feign源码

商城业务-商品上架-抽取响应结果&上架测试完成

分布式应用经常会因为服务不稳定,导致服务不可用。在服务启动第一次的时候,使用商品上架功能后台接口容易超时,多试几次就好了。

关于商品上架这一块业务比较复杂,我在后续会整理一张流程图。

商城业务-首页-整合thymeleaf渲染首页

服务端的页面渲染式开发。

pom.xml(zheli-product)

1
2
3
4
5
<!-- 模板引擎:thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

application.yml(zheli-product)

1
2
3
spring:
thymeleaf:
cache: false

目录结构:静态资源放static、html页面放templates。

1
2
3
resources/
static/
templates/

创建用来存放页面跳转的包com.aiz.zhelimall.product.web
把controller包改名为app。app包下面存放RESTful接口。web存放所有的controller。

使用模板引擎总结
(一).thymeleaf-starter:关闭缓存
(二).静态资源都放在static文件下就可以按照路径直接访问
(三).页面都放在templates下,直接访问
SpringBoot 访问项目的时候,默认会找index
可以参考WebMvcAutoConfiguration.welcomePageHandlerMapping()这个方法看。就可以理解为什么访问localhost:8000会转到resources/templates/index.html

商城业务-首页-整合dev-tools渲染一级分类数据

IndexController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Controller
public class IndexController {
@Autowired
CategoryService categoryService;
/* 首页面跳转 */
@GetMapping({"/","/index.html"})
public String indexPage(Model model){
// TODO 1.查出所有1级分类
List<CategoryEntity> categoryEntityList = categoryService.getLeve1Categorys();
model.addAttribute("categorys", categoryEntityList);
/*
* 默认前缀spring:thymeleaf:prefix = classpath:/templates/
* 默认后缀spring:thymeleaf:prefix = .html
* 视图解析器进行拼串:classpath:/templates/ + 返回值 + .html
*/
return "index";
}
}

由于我们使用的是thymeleaf模板引擎进行解析,这个类似于JSP,它也有自己的语法,比如${}取值变量等。想了解更多肯定得去官网了解一下。官网

首先给html文件加上thymeleaf的名称空间。

1
<html lang="en" xmlns:th="http://www.thymeleaf.org">

自定义属性和原生属性显示方式。

1
2
3
4
5
<ul>
<li th:each="category : ${categorys}">
<a href="#" class="header_main_left_a" th:attr="ctg-data=${category.catId}"><b th:text="${category.name}">家用电器</b></a>
</li>
</ul>

为了页面修改不重启服务器实时更新。引入dev-tools。

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>

资源编译Build Project (Ctrl+F9)
只对当前资源编译 (Ctrl+Shift+F9)

商城业务-首页-渲染二级三级分类数据

商城业务-nginx-搭建域名访问环境一(反向代理配置)

商城业务-nginx-搭建域名访问环境二(负载均衡到网关)

评论