谷粒商城之高级篇
目录
- 谷粒商城之高级篇
- 前言
- 2 商城业务
-
- 2.1 商品上架
- 2.2 商城系统首页
- 2.3 检索业务
- 2.4 商品详情
- 2.5 认证服务
-
- 2.5.1 环境搭建
- 2.5.2 短信验证码
- 2.5.3 验证码之防刷校验
- 2.5.4 一步一坑的注册页环境
- 2.5.5 异常机制
- 2.5.6 [MD5](https://so.csdn.net/so/search?q=MD5&spm=1001.2101.3001.7020)&盐值&BCrypt
- 2.5.7 注册完成
- 2.5.8 账户密码登录完成
- 2.5.9 社交登录
- 2.5.10 分布式session
- 2.5.11 SpringSession 整合 redis 完成 session域问题
- 2.5.12 SpringSession原理--装饰者模式
- 2.5.13 页面效果完善
- 2.5.14 单点登录
前言
高级篇正式开始啦!
PS 第一章 ElasticSearch 参见 另外一篇文章 谷粒商城之高级篇知识补充。
2 商城业务
2.1 商品上架
2.1.1 商品Mapping
ES是将数据存储在内存中,所以在检索中优于mysql。ES也支持集群,数据分片存储。
需求:
上架的商品才可以在网站上展示,没有上架的商品存储在数据库中。
上架的商品需要可以被检索。
分析sku在es中如何存储:
商品mapping
分析:商品上架在es中是存sku还是spu?
1)检索的时候输入名字,是需要按照sku的title进行全文检索的
2)检索使用商品规格,规格是spu的公共属性,每个spu是一样的
3)按照分类id进去的都是直接列出spu的,还可以切换。
4〕我们如果将sku的全量信息保存到es中(包括spu属性〕就太多字段了
方案1:方便检索
{
skuId:1
spuId:11
skyTitile:华为xx
price:999
saleCount:99
attr:[
{
尺寸:5},
{
CPU:高通945},
{
分辨率:全高清}
]
}
缺点:如果每个sku都存储规格参数(如尺寸),会有冗余存储,因为每个spu对应的sku的规格参数都一样
冗余:
举例:100万*20=2000MB=2G
方案2:分布式
sku索引
{
spuId:1
skuId:11
xxx
}
attr索引
{
skuId:11
attr:[
{
尺寸:5},
{
CPU:高通945},
{
分辨率:全高清}
]
}
举例:
先找到4000个符合要求的spu,再根据4000个spu查询对应的属性,封装了4000个id,long 8B*4000=32000B=32KB
1K个人检索,就是32MB
结论:如果将规格参数单独建立索引,会出现检索时出现大量数据传输的问题,会引起网络拥堵
因此选用方案1,以空间换时间
建立product索引
最终选用的数据模型:
PUT product
{
"mappings":{
"properties": {
"skuId":{
"type": "long" },
"spuId":{
"type": "keyword" }, # 不可分词
"skuTitle": {
"type": "text",
"analyzer": "ik_smart" # 中文分词器
},
"skuPrice": {
"type": "keyword" },
"skuImg" : {
"type": "keyword" },
"saleCount":{
"type":"long" },
"hasStock": {
"type": "boolean" },
"hotScore": {
"type": "long" },
"brandId": {
"type": "long" },
"catalogId": {
"type": "long" },
"brandName": {
"type": "keyword"},
"brandImg":{
"type": "keyword",
"index": false, # 不可被检索,不生成index
"doc_values": false # 不可被聚合
},
"catalogName": {
"type": "keyword" },
"attrs": {
# attrs:当前sku的属性规格
"type": "nested",
"properties": {
"attrId": {
"type": "long" },
"attrName": {
"type": "keyword",
"index": false,
"doc_values": false
},
"attrValue": {
"type": "keyword" }
}
}
}
}
}
其中
“type”: “keyword” 保持数据精度问题,可以检索,但不分词
“index”:false 代表不可被检索
“doc_values”: false 不可被聚合,es就不会维护一些聚合的信息
冗余存储的字段:不用来检索,也不用来分析,节省空间
库存是bool。
检索品牌id,但是不检索品牌名字、图片
用skuTitle检索
nested嵌入式对象
属性是"type": “nested”,因为是内部的属性进行检索
数组类型的对象会被扁平化处理(对象的每个属性会分别存储到一起)
user.name=[“aaa”,“bbb”]
user.addr=[“ccc”,“ddd”]
这种存储方式,可能会发生如下错误:
错误检索到{aaa,ddd},这个组合是不存在的
数组的扁平化处理会使检索能检索到本身不存在的,为了解决这个问题,就采用了嵌入式属性,数组里是对象时用嵌入式属性(不是对象无需用嵌入式属性)
nested阅读:https://blog.csdn.net/weixin_40341116/article/details/80778599
参考使用聚合:https://blog.csdn.net/kabike/article/details/101460578
课件内容:
分析:商品上架在es 中是存sku 还是spu?
1)、检索的时候输入名字,是需要按照sku 的title 进行全文检索的
2)、检索使用商品规格,规格是spu 的公共属性,每个spu 是一样的
3)、按照分类id 进去的都是直接列出spu 的,还可以切换。
4)、我们如果将sku 的全量信息保存到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 的数据,,传输阻塞时间会很
长,业务更加无法继续。
所以,我们如下设计,这样才是文档区别于关系型数据库的地方,宽表设计,不能去考虑数
据库范式。
index:
默认true,如果为false,表示该字段不会被索引,但是检索结果里面有,但字段本身不能
当做检索条件。
doc_values:
默认true,设置为false,表示不可以做排序、聚合以及脚本操作,这样更节省磁盘空间。
还可以通过设定doc_values 为true,index 为false 来让字段不能被搜索但可以用于排序、聚
合以及脚本操作:
2.1.2 上架细节
上架是将后台的商品放在es 中可以提供检索和查询功能。
1)、hasStock:代表是否有库存。默认上架的商品都有库存。如果库存无货的时候才需要
更新一下es
2)、库存补上以后,也需要重新更新一下es
3)、hotScore 是热度值,我们只模拟使用点击率更新热度。点击率增加到一定程度才更新
热度值。
4)、下架就是从es 中移除检索项,以及修改mysql 状态
商品上架步骤:
1)、先在es 中按照之前的mapping 信息,建立product 索引。
2)、点击上架,查询出所有sku 的信息,保存到es 中
3)、es 保存成功返回,更新数据库的上架状态信息。
2.1.3 数据一致性
1)、商品无库存的时候需要更新es 的库存信息
2)、商品有库存也要更新es 的信息
2.1.4 代码实现
POST /product/spuinfo/{spuId}/up
- SpuInfoController:
/**
* /product/spuinfo/{spuId}/up
* 商品上架功能
*/
@PostMapping("/{spuId}/up")
public R spuUp(@PathVariable("spuId") Long spuId){
spuInfoService.up(spuId);
return R.ok();
}
product里组装好,search里保存到es中,进行商品上架
- 商品上架entity
商品上架需要在es中保存spu信息并更新spu的状态信息,由于SpuInfoEntity与索引的数据模型并不对应,所以我们要建立专门的vo进行数据传输。
//商品在 es中保存的数据模型
@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<Attrs> attrs;
@Data
public static class Attrs {
private Long attrId;
private String attrName;
private String attrValue;
}
}
- 商品上架service
sku的规格参数相同,因此我们要将查询规格参数提前,只查询一次
1)在ware微服务里添加"查询sku是否有库存"的controller
WareSkuController
//查询sku 是否有库存
@PostMapping("/hasstock")
public R getSkuHasStock(@RequestBody List<Long> skuIds){
//sku_id,stock
List<SkuHasStockVo> vos = wareSkuService.getSkuHasStock(skuIds);
return R.ok().setData(vos);
}
WareSkuServiceImpl
@Override
public List<SkuHasStockVo> getSkuHasStock(List<Long> skuIds) {
List<SkuHasStockVo> collect = skuIds.stream().map(skuId -> {
SkuHasStockVo vo = new SkuHasStockVo();
//查询当前 sku的总库存量
//SELECT SUM(stock-stock_locked) FROM `wms_ware_sku` WHERE sku_id = 1
Long count = baseMapper.getSkuStock(skuId);
vo.setSkuId(skuId);
vo.setHasStock(count==null?false:count>0);
return vo;
}).collect(Collectors.toList());
return collect;
}
WareSkuDao
Long getSkuStock(Long skuId);//一个参数的话,可以不用写@Param,多个参数一定要写,方便区分
WareSkuDao.xml
</update>
<select id="getSkuStock" resultType="java.lang.Long">
SELECT SUM(stock-stock_locked) FROM `wms_ware_sku` WHERE sku_id = #{skuId}
</select>
SkuHasStockVo
@Data
public class SkuHasStockVo {
private Long skuId;
private Boolean hasStock;
}
然后用feign调用
在 package com.atguigu.gulimall.product.feign下:
@FeignClient("gulimall-ware") //说明调用哪一个 远程服务
public interface WareFeignService {
/**
* 1、R设计的时候可以加上泛型
* 2、直接返回我们想要的结果
* 3、自己封装解析结果
* @param skuIds
* @return
*/
@PostMapping("/ware/waresku/hasstock")//注意路径复制完全
R getSkuHasStock(@RequestBody List<Long> skuIds);
}
2)将 R 工具类进行改装
public class R extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
//利用 阿里巴巴提供的fastjson 进行逆转
public <T> T getData(TypeReference<T> typeReference){
Object data = get("data");//默认是map
String s = JSON.toJSONString(data);
T t = JSON.parseObject(s, typeReference);
return t;
}
public R setData(Object data){
put("data",data);
return this;
}
...
3)收集成map的时候,toMap()参数为两个方法,如 SkyHasStockVo::getSkyId,item->item.getHasStock()
将封装好的SkuInfoEntity,调用search的feign,保存到es中
ElasticSaveController
@Slf4j
@RequestMapping("/search/save")
@RestController
public class ElasticSaveController {
@Autowired
ProductSaveService productSaveService;
//上架商品
// 添加@RequestBody 将 请求体中的 List<SkuEsModel> 集合转换为json数据,因此请求方式必须为 @PostMapping
@PostMapping("/product")
public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels){
// 如果返回的是 boolean 类型的false,说明我们的 sku数据有问题
//如果返回的是 catch里面的内容,可能是 es 客户端连接不上了
boolean b = false;
try {
b = productSaveService.productStatusUp(skuEsModels);
}catch (Exception e){
log.error("ElasticSaveController商品上架错误: {}",e);
return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg());
}
if (!b){
return R.ok();
}else {
return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg());
}
}
}
ProductSaveServiceImpl
@Slf4j
@Service
public class ProductSaveServiceImpl implements ProductSaveService {
@Autowired
RestHighLevelClient restHighLevelClient;
@Override
public boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException {
//保存到es
//1.给 es 中建立索引。product,建立好映射关系。
//2.给 es 中保存这些数据
BulkRequest bulkRequest = new BulkRequest();
for (SkuEsModel model : skuEsModels) {
//1.构造保存请求
IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
indexRequest.id(model.getSkuId().toString());
String s = JSON.toJSONString(model);
indexRequest.source(s, XContentType.JSON);
bulkRequest.add(indexRequest);
}
//BulkRequest bulkRequest, RequestOptions options
BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
//TODO 1、如果批量错误
boolean b = bulk.hasFailures();
List<String> collect = Arrays.stream(bulk.getItems()).map(item -> {
return item.getId();
}).collect(Collectors.toList());
log.info("商品上架完成:{},返回数据:{}",collect,bulk.toString());
return b;
}
}
EsConstant
public class EsConstant {
public static final String PRODUCT_INDEX = "product"; //sku数据在 es中的索引
}
fenign 调用: gulimall-product 调用 gulimall-search
SearchFeignService
@FeignClient("gulimall-search")
public interface SearchFeignService {
@PostMapping("/search/save/product")
R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels);
}
4)上架失败返回R.error(错误码,消息)
此时再定义一个错误码枚举。在接收端获取他返回的状态码
BizCodeEnume
PRODUCT_UP_EXCEPTION(11000,"商品上架异常");
5)上架后再让数据库中变为上架状态
这里在 gulimall-common 包下的 ProductConstant 创建一个新的枚举类
public class ProductConstant {
...
public enum StatusEnum {
NEW_SPU(0,"新建"), SPU_UP(1,"商品上架"),SPU_DOWN(2,"商品下架");
private int code;
private String msg;
StatusEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
}
6)mybatis为了能兼容接收null类型,要把long
改为Long
debug时很容易远程调用异常,因为超时了
商品上架代码
SpuInfoController
/**
* /product/spuinfo/{spuId}/up
* 商品上架功能
*/
@PostMapping("/{spuId}/up")
public R spuUp(@PathVariable("spuId") Long spuId){
spuInfoService.up(spuId);
return R.ok();
}
SpuInfoServiceImpl
@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 -> {
//返回所有属性的id
return attr.getAttrId();
}).collect(Collectors.toList());
List<Long> searchAttrIds = attrService.selectSearchAttrIds(attrIds);
Set<Long> idSet = new HashSet<>(searchAttrIds);//因为是kv 键值对,转换成 set 集合比较方便
// 从 baseAttrs 集合中 过滤 出 attrValueEntities 集合
List<SkuEsModel.Attrs> attrsList = baseAttrs.stream().filter(item -> {
return idSet.contains(item.getAttrId());
}).map(item -> {
//将 set集合 映射 成 map集合
SkuEsModel.Attrs attrs = new SkuEsModel.Attrs();
BeanUtils.copyProperties(item, attrs);//属性对拷:item 是数据库中查出来的数据
return attrs;
}).collect(Collectors.toList());
//TODO 1、发送远程调用,库存系统查询是否有库存
//由于远程调用可能出现网络问题,所以需要进行try - catch处理一下
Map<Long, Boolean> stockMap = null;
try {
R r = wareFeignService.getSkuHasStock(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 -> {
//通过 stream API 将 skus中的 数据遍历
//组装我们需要的数据
SkuEsModel esModel = new SkuEsModel();
BeanUtils.copyProperties(sku, esModel);//属性对拷,将 sku中的属性 拷贝到 esmodel中
//需要单独处理的数据 ,SkuInfoEntity 和 SkuEsModel中相比少的数据。
//skuPrice,skuImg
esModel.setSkuPrice(sku.getPrice());
esModel.setSkuImg(sku.getSkuDefaultImg());
//hotScore(热度评分) hasStock(库存)
//设置库存信息
//如果远程调用出现问题,默认给 true值;如果没有问题,那就赋真正的值
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());
//设置检索属性
esModel.setAttrs(attrsList);
return esModel;
}).collect(Collectors.toList());
//TODO 5、将数据发送给 es 进行保存,gulimall-search
R r = searchFeignService.productStatusUp(upProducts);
if (r.getCode() == 0){
//远程调用成功
//TODO 6、修改当前spu的状态
baseMapper.updataSpuStatus(spuId, ProductConstant.StatusEnum.SPU_UP.getCode());
}else {
//远程调用失败
//TODO 7、重复调用?接口幂等性;重试机制? xxx
//Feign 调用流程
/**
* 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
这里再次 将 feign 接口代码展示出来:
gulimall-product 调用 gulimall-search 将 商品上架内容保存在 ElasticSearch中,方便全文检索:
SearchFeignService
@FeignClient("gulimall-search")
public interface SearchFeignService {
@PostMapping("/search/save/product")
R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels);
}
gulimall-product 调用 gulimall-ware 将 查询 商品库存:
WareFeignService
@FeignClient("gulimall-ware") //说明调用哪一个 远程服务
public interface WareFeignService {
/**
* 1、R设计的时候可以加上泛型
* 2、直接返回我们想要的结果
* 3、自己封装解析结果
* @param skuIds
* @return
*/
@PostMapping("/ware/waresku/hasstock")//注意路径复制完全
R getSkuHasStock(@RequestBody List<Long> skuIds);
}
ps:
这里可以用到的idea 快捷键:
- ctrl + e 可以快速调出最近使用的(打开最近修改的文件)
快速从 controller 跳转 到 实现类
ctrl + shift + 鼠标左键
从 controller 跳转到 接口
ctrl + 鼠标左键
生成 try-catch等(surround with)
alt + shift +z
生成构造器/get/set/toString
alt + shift + s
7)效果展示
商品成功上架,显示状态 为 已上架
2.2 商城系统首页
不使用前后端分离开发了,管理后台用vue
nginx发给网关集群,网关再路由到微服务静态资源放到nginx中
2.2.1 渲染首页
- 依赖
导入thymeleaf依赖
<!--模板引擎:thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
- html\首页资源\index 放到 gulimall-product 下的static文件夹
index.html 放到 templates中
- 关闭thymeleaf缓存,方便开发实时看到更新
thymeleaf:
cache: false
- web开发放到web包下,原来的controller是前后分离对接手机等访问的,所以可
以改成app,对接app应用。
web 包:存放专门进行页面跳转的controller
rest 接口对接的使我们分离的项目(比如手机的一些 app ):将controller 改名为 app
- 效果展示:访问首页
2.2.2 渲染一级分类数据
编写 处理首页的controller
gulimall-product的 web 包下新建 IndexController
@Controller
public class IndexController {
@Autowired
CategoryService categoryService;
@GetMapping({
"/","/index.html"})
public String indexPage(Model model){
//TODO 1.查出所有的1级分类
List<CategoryEntity> categoryEntities = categoryService.getLevel1Categorys();
//spring mvc提供了一个 model 接口
// 给 model 中放的数据,就会默认放到页面的请求域中,因为是转发。所以使用addAttribute
//给首页 放一个属性 ,属性名: categorys 属性值:categoryEntities------以后来到 index页面,就可以直接取出 属性。
model.addAttribute("categorys",categoryEntities);
// 如果返回的 是 逻辑视图(也就是页面地址) ,就会进行拼串
//视图解析器进行拼串:
//classpath:/ 表示类路径下 :resources下:文件夹右下角 有一个小图标
//默认规则:默认前缀:public static final String DEFAULT_PREFIX = "classpath:/templates/";
// 默认后缀:public static final String DEFAULT_SUFFIX = ".html";
// classpath:/templates/ + 返回值 + .html
return "index";
}
}
编写 获取 1级分类的实现
CategoryServiceImpl
/**
* 查找 1级分类
* parent_cid = 0 或者 cat_level = 1
* @return
*/
@Override
public List<CategoryEntity> getLevel1Categorys() {
List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
return categoryEntities;
}
引入 热部署依赖devtools使页面实时生效
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
首页遍历一级分类菜单数据
修改 index.html
<!--轮播主体内容-->
<div class="header_main">
....
<div class="header_main_left">
<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>
</div>
......
thymeleaf 知识小补充(复习):
thymeleaf官网:https://www.thymeleaf.org/
- ${}:动态取值
th:text="${category.name}"
- th:each:遍历
<tr th:each="prod : ${prods}">
prod : 当前元素
${prods}:要遍历的对象
th:each="category : ${categorys}"
- 自定义属性:我们需要获得 分类的 id
<input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
value:属性名叫什么
#{subscribe.submit}:属性值叫什么
th:attr="ctg-data=${category.catId}"
原生属性:
th:value="#{subscribe.submit}"
效果展示:
2.2.3 渲染二级三级分类数据
当 鼠标滑到 1级分类时,展示 它的二级分类数据及三级分类数据。
利用 catalogLoader.js
来获取请求,解析展示数据。
按照 此json 数据方式
新建 Catelog2Vo封装 数据
/**
* 2级分类 vo
*
* @author wystart
* @create 2022-11-24 21:53
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Catelog2Vo {
private String catalog1Id;//1级分类id
private List<Catelog3Vo> catalog3List; //三级子分类
private String id;
private String name;
/**
* 三级分类 vo
* "catalog2Id":"61",
* "id":"610",
* "name":"商务休闲鞋"
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
public static class Catelog3Vo {
private String catalog2Id; //父分类,2级分类 id
private String id;
private String name;
}
}
IndexController
//index/catalog.json
@ResponseBody
@GetMapping("/index/catalog.json")
public Map<String, List<Catelog2Vo>> getCatalogJson() {
Map<String, List<Catelog2Vo>> catalogJson = categoryService.getCatalogJson();
return catalogJson;
}
CategoryServiceImpl
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
//1.查出所有1级分类
List<CategoryEntity> level1Categorys = getLevel1Categorys();
//2.封装数据
Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
//1.每一个的一级分类,查到这个一级分类的二级分类
List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getParentCid()));
//2.封装上面的结果
List<Catelog2Vo> catelog2Vos = null;
if (categoryEntities != null) {
catelog2Vos = categoryEntities.stream().map(l2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
//1.找到当前二级分类的三级分类,封装成 vo
List<CategoryEntity> level3Catelog = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", l2.getCatId()));
// 三级分类有数据的情况下
if (level3Catelog != null){
List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
//2.封装成指定格式
Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
return catelog3Vo;
}).collect(Collectors.toList());
catelog2Vo.setCatalog3List(collect);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
return parent_cid;
}
效果展示:
访问 http://localhost:10000/index/catalog.json
得到 json 数据
首页展示效果:http://localhost:10000
模板引擎总结
* 5.模板引擎 * 1)、thymeleaf-starter: 关闭缓存 * 2)、静态资源都放在static 文件夹下就可以按照路径直接访问 * 3)、页面放在 templates下,直接访问 * SpringBoot,访问项目的时候,默认会找 index * 4)、页面修改不重启服务器实时更新 * 1)、引入 dev-tools * 2)、修改完页面 ctrl + shift + f9 或者 ctrl + f9,重新自动编译下页面(注意:如果代码配置等修改,建议重启) *
2.2.4 nginx 搭建域名访问环境
我们利用反向代理:让 Nginx 配合网关 搭建我们的访问环境,将我们的各个微服务放在内网中,避免端口直接暴露带来的危险。
利用 SwitchHosts软件可以快速修改hosts文件,注意需要以管理员身份运行。
原理:
查看本机localhost对应的IP地址:
ipconfig:查看本机IP:ipconfig:
192.168.1.103 (windows的localhost地址)
192.168.56.1(linux虚拟机的localhost地址)
两者都可以,都算本机
Nginx的配置文件详解:
可以加载 外部配置文件的配置,这样可以避免Nginx的配置文件过大。(总配置文件)
- Nginx 反向代理配置
接下来我们配置 server块:
先复制一份,留作备份:
修改配置文件:
proxy_pass:代理通过:相当于代理给谁(转交给谁)
gulimall下的所有请求都代理给 192.168.56.1下的10000端口。
Nginx的所有配置都以 ; 结尾,否则报错。
通过域名访问:gulimall.com
原理解析:
1.首先浏览器访问 gulimall.com----我们在windows里面指定了 gulimall.com 映射的是虚拟机IP:192.168.56.10,所以浏览器访问 gulimall.com 先会来到我们的虚拟机;
2.虚拟机里面的 Nginx又监听了80端口,在Nginx的配置文件中,它监听了来自80端口的所有请求,而且域名是 gulimall.com;所以符合以上条件,Nginx就会帮我们代理到我们本机:proxy_pass http://192.168.56.1:10000;
3.最后我们就又回到了本机
4.最后总结就是:域名来到 Nginx,Nginx 配置了gulimall.com ,代理到10000端口服务;
分布式情况下:商城系统有很多,不止一个,那需要每次修改 Nginx的代理配置?
太麻烦!!!
让Nginx 将请求代理给网关,由网关自动转发给我们各个服务;网关就能动态发现哪些服务上线,哪些服务下线;而且网关还具有负载均衡功能。
Nginx将请求交给网关,由网关从注册中心动态发现商品服务都在那,进而由网关负载均衡到商品服务;
网关也会部署多个,Nginx可以将请求负载均衡到某一个网关,然后由网关在进行转发。
-
Nginx 搭配网关 实现 负载均衡到网关
-
Nginx
修改 总配置 nginx.conf 在 http 块内:
在server 块内:
修改 server配置:gulimall.conf:相当于 是 负载均衡的配置,直接路由到上游服务器网关,由网关进行转发
效果就是:访问 gulimall.com ,代理 给 Nginx ,Nginx 转交 给网关 ,网关再转给商品服务。
-
网关配置:
- id: gulimall_host_route uri: lb://gulimall-product predicates: - Host=**.gulimall.com,gulimall.com # 只要是 gulimall下的所有请求都转给 gulimall-product
注意这个配置 一定要放在 最后:因为如果放在前面 ,它会禁用下面其他的网关配置:比如,http://gulimall.com//product/attrattrgrouprelation/list 这个api 接口访问,它会首先到 gulimall.com,然后因为没有进行 截串 设置(截取 /api前缀),出现 404 访问不到。
-
-
测试效果
这里出现 404 问题:原因:Nginx 转发给网关的时候,会丢失很多请求头信息,这里就缺失了 host 地址,这里我们暂时只配置 上 host 地址,以后缺啥补啥。
重启测试:
直接访问域名成功:gulimall.com
访问接口 也成功。http://gulimall.com//product/attrattrgrouprelation/list
最后总结:
最终原理:
首先浏览器访问 gulimall.com
因为我们在Windows配置了host映射:gulimall.com 映射IP 192.168.56.10(虚拟机Ip)
所以会直接来到虚拟机又因为 浏览器访问 默认不带端口,那就是访问80端口,所以会来到 Nginx,我们又配置 了 80端口监听 gulimall.com 这个域名;此外由于 **location/**下的配置:代理转发:
Nginx 又代理给网关,这里注意一个细节:由于Nginx 转发会丢失 一些请求头信息,所以我们要加上请求头的配置,这里暂时只配置 host地址,之后的其他请求头配置我们用到的时候在进行添加;
网关发现 域名 是gulimall.com,进而就会找到 对应的配置:路由到商品服务,进而就转给了商品服务,这处网关配置一定要放在最后面,避免放在前面禁用后面的其他截串配置。
- 域名映射效果:
- 请求接口 gulimall.com
- 请求页面 gulimall.com
- nginx 直接代理给网关,网关判断
- 如果是/api/***,转交给对应的服务器
- 如果是满足域名,转交给对应的服务
重要!!!!
关于 第3章 性能与压力测试 和 第4章 缓存与分布式锁单独写在另外一篇文档:谷粒商城之高级篇知识补充。
2.3 检索业务
2.3.1 页面环境搭建
①秉承动静分离的原则,我们将 静态资源放到 Nginx下:
在Nginx新建一个 文件夹search,用来存放相关静态资源。
②修改 index页面下的静态资源前缀
静态资源
加上 thymeleaf 的名称空间
③域名映射
④ *.gulimall.com 表示所有请求Nginx都处理,最后的结果就是Nginx转发给 网关
最终的转发效果就是:
⑤网关配置
- id: gulimall_host_route
uri: lb://gulimall-product
predicates:
- Host=gulimall.com #这里和之前的相比有修改
- id: gulimall_search_route
uri: lb://gulimall-search
predicates:
- Host=search.gulimall.com
⑥访问 http://search.gulimall.com/
2.3.2 调整页面跳转
为了以后开发方便,我们加上 热部署依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
关闭thymeleaf 的缓存
spring.thymeleaf.cache=false
我们可以通过检索页面的以下两个地方跳转会商城首页
一个是超链接,一个是图标。
首先是超链接修改:
search服务里面的index页面,改为 http://gulimall.com
接着是图标处:改为 http://gulimall.com
修改域名映射:让gulimall.com和带其子域名的都转发给网关。
测试:成功跳转回首页。
接下来,我们在首页上可以有这么两个地方,可以跳转到我们的检索页面:
①关键字搜索:搜索按钮
②点击分类,跳转到检索页面。
③修改配置:
-
通过分类点击到检索页面
-
将 检索页面 重命名为 list.html
-
创建SearchController
@Controller public class SearchController { @GetMapping("/list.html") public String listPage() { return "list"; } }
-
避坑:
如果点击 分类跳转 到 检索页面,报错,然后控制台域名是:search.gmall.com开头,那么我们需要去
Nginx 下的 html/static/index/js,在 catelogLoader中搜索gmall,替换为 gulimall
-
-
通过首页的搜索图标跳转到检索页面
修改 gulimall-product下的index.html页面:
搜索 search:
search方法应该是这样:之前修改前缀的时候多加了/static,所以一直访问不到,下面这个是正确的。
另外图标处修改为:
ps: 注意一定要把product商品服务中的application.yaml配置文件中 thymeleaf 的页面缓存设置为false,之前测试缓存的时候给设为 开启了,开发中我们关闭。
- 测试,都成功跳转到检索页面。
ps:测试的时候,注意浏览器缓存问题,不然有时候测试不成功。
2.3.3 检索返回结果模型分析抽取
1、检索业务分析
商品检索三个入口:
1)、选择分类进入商品检索
2)、输入检索关键字展示检索页
3)、选择筛选条件进入
检索条件&排序条件
- 全文检索:skuTitle
- 排序: saleCount、hotScore、skuPrice
- 过滤:hasStock、skuPrice 区间、brandId、catalogId、attrs
- 聚合:attrs
完整的url 参数
keyword=小米&sort=saleCount_desc/asc&hasStock=0/1&skuPrice=400_1900&brandId=1
&catalogId=1&attrs=1_3G:4G:5G&attrs=2_骁龙845&attrs=4_高清屏
修改 SearchController
@Controller
public class SearchController {
@Autowired
MallSearchService mallSearchService;
/**
* 创建SearchParam:避免controller 方法参数位置接收太多的请求参数
* 自动将页面提交过来的所有请求查询参数封装成指定的对象
* @param param
* @return
*/
@GetMapping("/list.html")
public String listPage(SearchParam param, Model model) {
//1、根据传递过来的页面的查询参数,去es中检索商品
SearchResult result = mallSearchService.search(param);
//放到 model 中,方便页面取值
model.addAttribute("result",result);
return "list";
}
}
创建 SearchParam类(vo包下):封装页面所有可能传递过来的查询条件:请求参数模型
/**
* 封装页面所有可能传递过来的查询条件
*
* catalog3Id=225&keyword=小米&sort=saleCount_asc&hasStock=0/1&brandId=1&brandId=2&attrs=1_5寸:6寸&attrs=2_16G:8G
*/
@Data
public class SearchParam {
private String keyword;//页面传递过来的全文匹配关键字
private Long catalog3Id;//页面传递过来的三级分类id
/**
* sort=saleCount_asc/desc
* sort=skuPrice_asc/desc
* sort=hostScore_asc/desc
*
*/
private String sort;//排序条件
/**
* 好多的过滤条件
* hasStock(是否有货)、skuPrice 区间、brandId、catalogId、attrs
* hasStock=0/1 :0有货;1无货
* skuPrice=1_500/500_/_500
* brandId=1
* attrs=2_5寸:6寸
*
*/
private Integer hasStock = 1;//是否只显示有货
private String skuPrice;//价格区间查询
private List<Long> brandId;//按照品牌进行查询,可以多选
private List<String> attrs;//按照属性进行筛选
private Integer pageNum = 1;//页码
}
创建 SearchResult :封装页面所有可能返回的结果:响应数据模型
/**
* 封装页面所有可能返回的结果
*/
@Data
public class SearchResult {
//查询到的所有商品信息
private List<SkuEsModel> products;
/**
* 以下是分页信息
*/
private Integer pageNum;//当前页码
private Long total;//总记录数
private Integer totalPages;//总页码
private List<BrandVo> brands;//当前查询到的结果,所有涉及到的品牌
private List<CatalogVo> catalogs;//当前查询到的结果,所有涉及到的所有分类
private List<AttrVo> attrs;//当前查询到的结果,所有涉及到的所有属性
//============================以上是返回给页面的所有信息============================
@Data
public static class BrandVo{
private Long brandId;
private String brandName;
private String brandImg;
}
@Data
public static class CatalogVo{
private Long catalogId;
private String catalogName;
}
@Data
public static class AttrVo{
private Long attrId;
private String attrName;
private List<String> attrValue;
}
}
创建 MallSearchService 及其实现
MallSearchService
public interface MallSearchService {
/**
*
* @param param 检索的所有参数
* @return 返回检索的结果,里面包含页面所需要的所有信息
*/
SearchResult search(SearchParam param);
}
MallSearchServiceImpl
@Service
public class MallSearchServiceImpl implements MallSearchService {
@Override
public Object search(SearchParam param) {
return null;
}
}
分析结果 见 上面的 SearchParam类及SearchResult类。
2.3.4 检索DSL语句
在 Kibana中进行检索DSL语句测试。
- 查询部分
最终检索语句:
GET product/_search
{
"query": {
"bool": {
"must": [
{
"match": {
#模糊匹配-全文检索
"skuTitle": "华为"
}
}
],
"filter": [ #过滤条件
{
"term": {
"catalogId": "225"
}
},
{
"terms": {
"brandId": [
"1",
"2",
"9"
]
}
},
{
"nested": {
#嵌套查询
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "15"
}
}
},
{
"terms": {
"attrs.attrValue": [
"海思(Hisilicon)",
"以官网信息为准"
]
}
}
]
}
}
}
},
{
"term": {
"hasStock": {
"value": "true"
}
}
},
{
"range": {
"skuPrice": {
"gte": 0,
"lte": 6000
}
}
}
]
}
},
"sort": [ #排序
{
"skuPrice": {
"order": "desc"
}
}
],
"from": 0, #分页
"size": 1,
"highlight": {
#高亮
"fields": {
"skuTitle": {
}
},
"pre_tags": "<b style='color:red'>",
"post_tags": "</b>"
}
}
整个查询条件:模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存),排序,分页,高亮,聚合分析。
- 接下来就是聚合分析部分。
这里我们希望可以通过品牌属性等也可以检索到商品。
所以加上 品牌属性等检索条件。
报错:
修改映射,让他们都可以进行聚合分析。
创建新的映射
PUT gulimall_product
{
"mappings": {
"properties": {
"attrs": {
"type": "nested",
"properties": {
"attrId": {
"type": "long"
},
"attrName": {
"type": "keyword"
},
"attrValue": {
"type": "keyword"
}
}
},
"brandId": {
"type": "long"
},
"brandImg": {
"type": "keyword"
},
"brandName": {
"type": "keyword"
},
"catalogId": {
"type": "long"
},
"catalogName": {
"type": "keyword"
},
"hasStock": {
"type": "boolean"
},
"hotScore": {
"type": "long"
},
"saleCount": {
"type": "long"
},
"skuId": {
"type": "long"
},
"skuImg": {
"type": "keyword"
},
"skuPrice": {
"type": "keyword"
},
"skuTitle": {
"type": "text",
"analyzer": "ik_smart"
},
"spuId": {
"type": "keyword"
}
}
}
}
数据迁移
# 数据迁移
POST _reindex
{
"source":{
"index":"product"
},
"dest":{
"index":"gulimall_product"
}
}
查询
GET gulimall_product/_search
迁移成功。
修改 EsConstant
代码
最终聚合分析语句:
"aggs": {
"brand_agg": {
"terms": {
"field": "brandId",
"size": 10
},
"aggs": {
"brand_name_agg": {
"terms": {
"field": "brandName",
"size": 10
}
},
"brand_img_agg":{
"terms": {
"field": "brandImg",
"size": 10
}
}
}
},
"catalog_agg":{
"terms": {
"field": "catalogId",
"size": 10
},
"aggs": {
"catalog_name_agg": {
"terms": {
"field": "catalogName",
"size": 10
}
}
}
},
"attr_agg":{
"nested": {
"path": "attrs"
},
"aggs": {
"attr_id_agg": {
"terms": {
"field": "attrs.attrId",
"size": 10
},
"aggs": {
"attr_name_agg": {
"terms": {
"field": "attrs.attrName",
"size": 10
}
},
"attr_value_agg":{
"terms": {
"field": "attrs.attrValue",
"size": 10
}
}
}
}
}
}
}
- 整个查询的检索DSL语句:
GET product/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"skuTitle": "华为"
}
}
],
"filter": [
{
"term": {
"catalogId": "225"
}
},
{
"terms": {
"brandId": [
"1",
"2",
"9"
]
}
},
{
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "15"
}
}
},
{
"terms": {
"attrs.attrValue": [
"海思(Hisilicon)",
"以官网信息为准"
]
}
}
]
}
}
}
},
{
"term": {
"hasStock": {
"value": "true"
}
}
},
{
"range": {
"skuPrice": {
"gte": 0,
"lte": 6000
}
}
}
]
}
},
"sort": [
{
"skuPrice": {
"order": "desc"
}
}
],
"from": 0,
"size": 1,
"highlight":