绩溪做网站,做机械设备哪个网站好,如何做多语言网站,企业网站设计模板文章目录 前言一、前置知识1、Elasticsearch 的结构2、倒排索引 (Inverted Index)2.1、 索引阶段2.2、查询阶段 二、环境准备1、安装Es2、安装Kibana3、安装 ik 分词器 三、项目整合1、引入依赖2、整合业务2.1、创建索引、文档、构建查询语句2.2、整合业务代码 后记 前言 本篇介… 文章目录 前言一、前置知识1、Elasticsearch 的结构2、倒排索引 (Inverted Index)2.1、 索引阶段2.2、查询阶段 二、环境准备1、安装Es2、安装Kibana3、安装 ik 分词器 三、项目整合1、引入依赖2、整合业务2.1、创建索引、文档、构建查询语句2.2、整合业务代码 后记 前言 本篇介绍谷粒商城项目检索服务从搭建es环境到商城检索业务的实现。不考虑freeMarker模版中的jquery部分 对应视频P173-P192 一、前置知识
1、Elasticsearch 的结构 同传统的关系型数据库进行类比
索引 (Index):相当于关系数据库中的表由多个文档组成。文档 (Document):文档是 Elasticsearch 中存储的基本数据单位相当于关系数据库中的一行。文档以 JSON 格式存储。每个文档属于一个特定的索引并具有唯一的 ID。映射 (Mapping):类似于数据库中的表结构定义定义了文档的字段及其数据类型。DDL建表语句
2、倒排索引 (Inverted Index) 在倒排索引中每个词项都关联到一个倒排列表Posting List该列表存储着包含这个词项的所有文档的 ID。倒排索引的构建和查询主要分为以下两个阶段
2.1、 索引阶段 当文档被添加到系统中时首先会进行文档解析分词然后将每个词项添加到倒排索引的词典中。词典存储的是文档中所有唯一词项的列表。最后对于每个词项记录该词项在哪些文档中出现存储这些文档的 ID 以及该词项在文档中的位置可选。这些信息称为倒排列表。 例如我现在有两个文档文档一布偶猫吃鱼和文档二加菲猫吃鱼分词后可以得到以下倒排索引假设目前使用的是ik分词器
词文档ID布偶1加菲猫2猫12吃鱼12
2.2、查询阶段 首先会进行分词布偶猫吃鱼会得到词项【布偶猫吃鱼】加菲猫吃鱼会得到词项【加菲猫吃鱼】。然后根据分出的词去查找其文档ID
布偶的文档ID是1。猫的文档ID是12。吃鱼的文档ID是12。加菲猫的文档ID是2。 最后找到同时包含所有查询词项的文档例如我要搜索布偶猫吃鱼文档1会作为结果返回。 二、环境准备
1、安装Es 在本项目中采用docker安装es的方式。es和kibana均采用7.4.2版本 首先执行
mkdir -p /mydata/elasticsearch/config
mkdir -p /mydata/elasticsearch/data然后执行
echo http.host: 0.0.0.0 /mydata/elasticsearch/config/elasticsearch.yml这条命令的含义是在 /mydata/elasticsearch/config/elasticsearch.yml 文件的末尾添加一行配置内容是http.host: 0.0.0.0会允许服务通过任何 IP 地址访问Elasticsearch 。
chmod -R 777 /mydata/elasticsearch/这条命令的含义是修改指定目录下的所有文件和子目录的权限。777 表示赋予所有用户文件的所有者、同组用户、其他用户读取r、写入w 和 执行x 的权限。权限的 777 是通过组合 rwx读、写、执行的权限位来得到的7 r w x 4 2 1即读、写、执行权限都有。 最后执行
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e discovery.typesingle-node \
-e ES_JAVA_OPTS-Xms64m -Xmx512m \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2- docker run启动一个新的 Docker 容器。 ---name elasticsearch给容器命名为 elasticsearch。 --p 9200:9200 -p 9300:9300-p 选项用于将主机的端口与容器内的端口进行映射。9200:9200将主机的 9200 端口映射到容器的 9200 端口Elasticsearch 默认的 HTTP REST API 端口。9300:9300将主机的 9300 端口映射到容器的 9300 端口Elasticsearch 默认的内部节点通信端口。 - -e discovery.typesingle-node-e 选项用于设置环境变量。“discovery.typesingle-node”指定 Elasticsearch 以单节点模式运行即不需要集群节点的发现通常用于开发或测试环境。 --e ES_JAVA_OPTS-Xms64m -Xmx512mES_JAVA_OPTS 是用于配置 JVM 的选项。-Xms64m设置 JVM 的最小堆内存为 64MB。-Xmx512m设置 JVM 的最大堆内存为 512MB。 --v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml-v 选项用于将主机上的目录或文件挂载到容器中。将主机上 /mydata/elasticsearch/config/elasticsearch.yml 文件挂载到容器内的 /usr/share/elasticsearch/configelasticsearch.yml用于替换容器中的默认配置文件。 - -v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml-v 选项用于将主机上的目录或文件挂载到容器中。将主机上/mydata/elasticsearch/config/elasticsearch.yml文件挂载到容器内的 /usr/share/elasticsearch/config/elasticsearch.yml用于替换容器中的默认配置文件。 --v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins将主机上的/mydata/elasticsearch/plugins目录挂载到容器的/usr/share/elasticsearch/plugins目录用于存储和管理 Elasticsearch 插件。 - -d elasticsearch:7.4.2-d 选项让容器在后台运行守护模式。elasticsearch:7.4.2指定要使用的 Elasticsearch 镜像及其版本号 7.4.2。
2、安装Kibana Kibana相当于es的可视化界面和控制台
docker run --name kibana -e ELASTICSEARCH_HOSTShttp://自己的虚拟机地址:9200 -p 5601:5601 \
-d kibana:7.4.2- docker run启动一个新的 Docker 容器。 ---name kibana 给容器命名为kibana。 -e ELASTICSEARCH_HOSTShttp://192.168.56.10:9200指定 Kibana 连接的 Elasticsearch 集群地址为 http://192.168.56.10:9200。 -p 5601:56015601:5601将主机的 5601 端口映射到容器的 5601 端口。 -d kibana:7.4.2指定使用 7.4.2 版本的 Kibana 镜像并使容器在后台运行。
3、安装 ik 分词器 将ik分词器拷贝到/mydata/elasticsearch/plugins/ik/下重启es容器可使用下面的命令验证是否安装成功
curl -X GET http://localhost:9200/_cat/plugins?v三、项目整合
1、引入依赖 在gulimall-search模块中引入依赖
dependencygroupIdorg.elasticsearch.client/groupIdartifactIdelasticsearch-rest-high-level-client/artifactIdversion7.4.2/version
/dependency
dependencygroupIdorg.elasticsearch/groupIdartifactIdelasticsearch/artifactIdversion7.4.2/version
/dependency
dependencygroupIdorg.elasticsearch.client/groupIdartifactIdelasticsearch-rest-client/artifactIdversion7.4.2/version
/dependency后续在项目中使用只需要注入RestHighLevelClient即可
Autowired
private RestHighLevelClient client;2、整合业务
2.1、创建索引、文档、构建查询语句 在分布式基础篇的后台管理系统中点击上架会将商品信息保存在gulimall_product索引中gulimall_product的映射注意分布式基础篇中映射是product和现在的gulimall_product有所不同需要进行数据迁移
{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}
}在页面上点击搜索时相当于带着搜索条件去ES中进行检索并且将检索的结果封装成对象返回给前端页面进行展示我们需要
对skuTitle进行模糊匹配高亮显示。对catalogIdbrandIdattrshasStockrange进行过滤。对skuPrice进行排序。进行分页。根据上面查询的结果进行聚合分析按照brandIdcatalogIdattrId查询出共有的部分。 其中must和filter的区别must 中的条件会参与相关性评分_score的计算。如果多个条件都出现在 must 中满足的条件越多相关性评分就越高。filter 只用于过滤文档满足条件的文档会返回但不会参与相关性评分的计算。因此它比 must 更高效特别适合用于不需要计算评分的精确匹配或过滤。 什么是相关性评分 相关性评分是 Elasticsearch 在返回搜索结果时用来衡量每个文档与查询条件的匹配程度的一个分值。每个文档返回时都有一个 _score 值表示这个文档与查询的匹配程度。分数越高意味着这个文档与查询条件越相关。 构建的查询语句
GET /gulimall_product/_search
{query: {bool: {must: [{match: {skuTitle: iphone}}],filter: [{term: {catalogId: {value: 225}}},{terms: {brandId: [8,9]}},{nested: {path: attrs,query: {bool: {must: [{term: {attrs.attrId: {value: 1}}},{terms: {attrs.attrValue: [5G,4G]}}]}}}},{term: {hasStock: {value: false}}},{range: {skuPrice: {gte: 4999,lte: 5400}}}]}},sort: [{skuPrice: {order: desc}}],from: 0,size: 10,highlight: {fields: {skuTitle:{}},pre_tags: b stylecolor:red,post_tags: /b},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: catelogName,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}}}}}}}
}对于其中一些关键点的解释
nested用于处理文档中的嵌套字段嵌套字段指的是ES文档中存储的对象类型的数据而普通的对象的ES中会被扁平化处理可能会导致错误匹配的现象。而定义一个字段为嵌套类型时Elasticsearch 会将每个嵌套对象视为独立的小文档但它仍然与父文档保持关联。
#定义嵌套字段
{mappings: {properties: {attrs: {type: nested, properties: {attrId: { type: integer },attrValue: { type: text }}}}}
}bash
# 查询嵌套字段
{nested: {path: attrs,query: {bool: {must: [{ term: { attrs.attrId: 1 } },{ term: { attrs.attrValue: 红色 } }]}}}
} aggs是一种用于计算统计信息、汇总数据的功能。可以对查询到的结果进行分析、分组、统计等操作aggs还可以嵌套使用即在一个聚合内部定义另一个聚合
#先根据 brandId 字段对文档进行分组然后对每个 brandId 组计算该组中文档的 price 字段的平均值。
{aggs: {brand_agg: {terms: {field: brandId},aggs: {avg_price: {avg: {field: price}}}}}
}
而对于嵌套字段的聚合需要进行特殊处理
# 首先对嵌套字段 attrs 进行聚合然后对 attrs.attrId 进行 terms 聚合
{aggs: {nested_attrs: {nested: {path: attrs},aggs: {attr_id: {terms: {field: attrs.attrId}}}}}
}
2.2、整合业务代码 到这里查询语句就已经构建完毕了但是还需要将查询语句转化成Java语言利用RestHighLevelClient发送请求进行查询并且解析返回结果 /*** 商品上架后信息保存在es-根据前端传递的搜索条件构建dsl去es中搜索-解析并封装查询结果给前端页面* param searchDTO 条件* return*/Overridepublic SearchVO searchForCondition(SearchDTO searchDTO) {//构建查询请求SearchRequest searchRequest this.getSearchRequest(searchDTO);SearchVO vo;try {//查询SearchResponse searchResponse client.search(searchRequest, RequestOptions.DEFAULT);System.out.println(返回的结果searchResponse.toString());//解析查询结果封装成SearchVOvo this.parseSearchRequest(searchResponse,searchDTO);} catch (Exception e) {throw new RuntimeException(e);}return vo;}查询 private SearchRequest getSearchRequest(SearchDTO searchDTO) {// 构建查询SearchSourceBuilder searchSourceBuilder new SearchSourceBuilder();// Bool查询BoolQueryBuilder boolQuery QueryBuilders.boolQuery();// 模糊匹配 skuTitlethis.likeSearch(searchDTO, boolQuery);//过滤查询this.filter(searchDTO, boolQuery, searchSourceBuilder);//分页高亮排序this.sortPageAndHighlight(searchDTO, searchSourceBuilder);//对搜索的结果进行聚合分析this.termsAggregation(searchSourceBuilder);//测试 打印构建结果String s searchSourceBuilder.toString();System.out.println(构建的 DSL s);//创建搜索请求SearchRequest searchRequest new SearchRequest(newString[]{ESConstants.PRODUCT_INDEX}, searchSourceBuilder);return searchRequest;}标题模糊匹配 private void likeSearch(SearchDTO searchDTO, BoolQueryBuilder boolQuery) {String keyword searchDTO.getKeyword();if (StringUtils.isNotBlank(keyword)) {boolQuery.must(QueryBuilders.matchQuery(skuTitle, keyword)); //对应es中的must}}过滤查询 private void filter(SearchDTO searchDTO, BoolQueryBuilder boolQuery, SearchSourceBuilder searchSourceBuilder) {// catalogId 查询Long catalog3Id searchDTO.getCatalog3Id();if (!ObjectUtils.isEmpty(catalog3Id)) {boolQuery.filter(QueryBuilders.termsQuery(catalogId, new long[]{catalog3Id})); //对应es中的filter}//bool - filter - 按照品牌 id 查询ListLong brandId searchDTO.getBrandId();if (!CollectionUtils.isEmpty(brandId)) {boolQuery.filter(QueryBuilders.termsQuery(brandId,brandId));}//bool - filter - 按照所有指定的属性进行查询ListString attrs searchDTO.getAttrs();if (!CollectionUtils.isEmpty(attrs)) {BoolQueryBuilder nestedboolQuery QueryBuilders.boolQuery(); //对应es中的nested//进行处理//attr1_5寸:8寸for (String attr : attrs) {String attrId attr.split(_)[0];String value attr.split(_)[1];
// String[] attrValues s.split(:);nestedboolQuery.must(QueryBuilders.termQuery(attrs.attrId,attrId));nestedboolQuery.must(QueryBuilders.termsQuery(attrs.attrValue,value));//每一个必须都得生成一个 nested 查询NestedQueryBuilder nestedQuery QueryBuilders.nestedQuery(attrs, nestedboolQuery, ScoreMode.None);boolQuery.filter(nestedQuery);}}//bool - filter - 按照库存是否有进行查询if (searchDTO.getHasStock() ! null) {boolQuery.filter(QueryBuilders.termQuery(hasStock,searchDTO.getHasStock() 1));}//1.2、bool - filter - 按照价格区间if (!StringUtils.isEmpty(searchDTO.getSkuPrice())) {//1_500/_500/500_/*** range: {* skuPrice: {* gte: 0,* lte: 6000* }* }*/RangeQueryBuilder rangeQuery QueryBuilders.rangeQuery(skuPrice); //对应es中的rangeString[] s searchDTO.getSkuPrice().split(_);if (s.length 2) {//区间rangeQuery.gte(s[0]).lte(s[1]);} else if (s.length 1) {if (searchDTO.getSkuPrice().startsWith(_)) {rangeQuery.lte(s[0]);}if (searchDTO.getSkuPrice().endsWith(_)) {rangeQuery.gte(s[0]);}}boolQuery.filter(rangeQuery);}//把以前的所有条件都拿来进行封装searchSourceBuilder.query(boolQuery);}分页高亮排序 private void sortPageAndHighlight(SearchDTO searchDTO, SearchSourceBuilder searchSourceBuilder) {//排序//sorthotScore_asc/descString sort searchDTO.getSort();if (StringUtils.isNotBlank(sort)) {String sortStr sort.split(_)[1];SortOrder order sortStr.equalsIgnoreCase(asc) ? SortOrder.ASC : SortOrder.DESC;searchSourceBuilder.sort(sort.split(_)[0], order);}//分页searchSourceBuilder.from((searchDTO.getPageNum() - 1) *ESConstants.PRODUCT_PAGESIZE);searchSourceBuilder.size(ESConstants.PRODUCT_PAGESIZE);//高亮if (!StringUtils.isEmpty(searchDTO.getKeyword())) {HighlightBuilder builder new HighlightBuilder();builder.field(skuTitle);builder.preTags(b stylecolor:red);builder.postTags(/b);searchSourceBuilder.highlighter(builder);}}聚合分析 private void termsAggregation(SearchSourceBuilder searchSourceBuilder) {/*** 聚合分析*///1、品牌聚合TermsAggregationBuilder brand_agg AggregationBuilders.terms(brand_agg);brand_agg.field(brandId).size(50);//品牌聚合的子聚合brand_agg.subAggregation(AggregationBuilders.terms(brand_name_agg).field(brandName).size(1));brand_agg.subAggregation(AggregationBuilders.terms(brand_img_agg).field(brandImg).size(1));/*1、聚合 brand*/searchSourceBuilder.aggregation(brand_agg);//2、分类聚合 catalog_aggTermsAggregationBuilder catalog_agg AggregationBuilders.terms(catalog_agg).field(catalogId).size(20);catalog_agg.subAggregation(AggregationBuilders.terms(catalog_name_agg).field(catalogName).size(1));/*2、聚合 catalog*/searchSourceBuilder.aggregation(catalog_agg);//3、属性聚合 attr_aggNestedAggregationBuilder attr_agg AggregationBuilders.nested(attr_agg, attrs);//聚合出当前所有的 attrIdTermsAggregationBuilder attr_id_agg AggregationBuilders.terms(attr_id_agg).field(attrs.attrId);//聚合分析出当前 attr_id 对应的名字attr_id_agg.subAggregation(AggregationBuilders.terms(attr_name_agg).field(attrs.attrName).size(1));//聚合分析出当前 attr_id 对应的所有可能的属性值 attrValueattr_id_agg.subAggregation(AggregationBuilders.terms(attr_value_agg).field(attrs.attrValue).size(50));attr_agg.subAggregation(attr_id_agg);/*3、聚合 attr*/searchSourceBuilder.aggregation(attr_agg);}解析返回结果 private SearchVO parseSearchRequest(SearchResponse resp, SearchDTO searchDTO) {SearchVO vo new SearchVO();//外层hitsSearchHits hits resp.getHits();//里层hitsSearchHit[] hitsArr hits.getHits();ListESPojo esPojos Arrays.stream(hitsArr).map(searchHit - {//_sourceString sourceAsString searchHit.getSourceAsString();ESPojo esPojo JSON.parseObject(sourceAsString, ESPojo.class);//判断是否按关键字检索若是就显示高亮否则不显示if (!StringUtils.isEmpty(searchDTO.getKeyword())) {//拿到高亮信息显示标题HighlightField skuTitle searchHit.getHighlightFields().get(skuTitle);String skuTitleValue skuTitle.getFragments()[0].string();esPojo.setSkuTitle(skuTitleValue);}return esPojo;}).collect(Collectors.toList());//返回的所有商品vo.setProducts(esPojos);//聚合信息ParsedLongTerms brandAgg resp.getAggregations().get(brand_agg);List? extends Terms.Bucket brandAggBuckets brandAgg.getBuckets();ListSearchVO.BrandsVO brandsVOS brandAggBuckets.stream().map(bucket - {SearchVO.BrandsVO brandsVO new SearchVO.BrandsVO();//得到品牌idlong brandId bucket.getKeyAsNumber().longValue();brandsVO.setBrandId(brandId);//得到品牌名字ParsedStringTerms brandNameAgg bucket.getAggregations().get(brand_name_agg);String brandName brandNameAgg.getBuckets().get(0).getKeyAsString();brandsVO.setBrandName(brandName);//3、得到品牌的图片ParsedStringTerms brandImgAgg bucket.getAggregations().get(brand_img_agg);String brandImg brandImgAgg.getBuckets().get(0).getKeyAsString();brandsVO.setBrandImg(brandImg);return brandsVO;}).collect(Collectors.toList());//封装品牌信息vo.setBrands(brandsVOS);ParsedLongTerms catalogAgg resp.getAggregations().get(catalog_agg);List? extends Terms.Bucket catalogAggBuckets catalogAgg.getBuckets();ListSearchVO.CatalogsVO catalogsVOS catalogAggBuckets.stream().map(bucket - {SearchVO.CatalogsVO catalogsVO new SearchVO.CatalogsVO();//得到分类ID//得到品牌idlong catelogId bucket.getKeyAsNumber().longValue();catalogsVO.setCatalogId(catelogId);//得到品牌名称ParsedStringTerms catalogNameAgg bucket.getAggregations().get(catalog_name_agg);String catalogName catalogNameAgg.getBuckets().get(0).getKeyAsString();catalogsVO.setCatalogName(catalogName);return catalogsVO;}).collect(Collectors.toList());//封装分类信息vo.setCatalogs(catalogsVOS);ParsedNested attrsAgg resp.getAggregations().get(attr_agg);ParsedLongTerms attrIdAgg attrsAgg.getAggregations().get(attr_id_agg);List? extends Terms.Bucket attrsAggBuckets attrIdAgg.getBuckets();ListSearchVO.AttrsVo attrsVoList attrsAggBuckets.stream().map(bucket - {SearchVO.AttrsVo attrsVo new SearchVO.AttrsVo();//属性IDlong attrId bucket.getKeyAsNumber().longValue();attrsVo.setAttrId(attrId);//属性名称ParsedStringTerms attrNameAgg bucket.getAggregations().get(attr_name_agg);attrsVo.setAttrName(attrNameAgg.getBuckets().get(0).getKeyAsString());//属性值ParsedStringTerms attrValueAgg bucket.getAggregations().get(attr_value_agg);ListString attrValues attrValueAgg.getBuckets().stream().map(item - item.getKeyAsString()).collect(Collectors.toList());attrsVo.setAttrValue(attrValues);return attrsVo;}).collect(Collectors.toList());//封装属性信息vo.setAttrs(attrsVoList);//封装分页参数vo.setPageNum(searchDTO.getPageNum());int total (int) resp.getHits().getTotalHits().value;vo.setTotal(total);int totalPages (int)total % ESConstants.PRODUCT_PAGESIZE 0 ?(int)total / ESConstants.PRODUCT_PAGESIZE : ((int)total / ESConstants.PRODUCT_PAGESIZE 1);vo.setTotalPages(totalPages);return vo;}到这里为止构建查询语句查询ES解析返回结果的操作就完成了。在高亮展示这一块有一个很坑的点如果在对标题进行匹配时是这样写的boolQuery.must(QueryBuilders.matchQuery(skuTitle, keyword)).fuzziness(Fuzziness.AUTO)); 实测高亮展示会失效。因为.fuzziness(Fuzziness.AUTO)是模糊匹配的一个配置选项表示自动确定模糊匹配的程度。下面简单说一下它的工作机制 对于长度较短的词1到2个字符不进行模糊匹配。对于长度为3到5个字符的词允许最多一个字符不同。也就是说输入的词和索引中的词之间最多可以有一个字符的差异。对于长度超过5个字符的词允许最多两个字符不同即输入的词和索引中的词之间可以有两处字符差异。 为什么加上了会使高亮展示失效因为模糊匹配会容忍一定的字符变化比如拼写错误或词形变化。高亮显示依赖于精确匹配的词只有当查询中的词语与索引中的词精确匹配时ES才会高亮显示。
后记 本篇主要介绍了ES的组成和基本概念以及环境搭建项目业务整合ES。因为项目的重点是后端的逻辑所以前端模板页面的改造没有写入本篇。在做这个项目之前去年有曾经专门去看过某马的关于ES的专题教学视频语法介绍的很详细当时还跟着敲了一遍。但是工作中至今未遇到使用场景在做这个项目的时候不出意外地发现几乎全部遗忘了。在这里想说的是语法并非重点重点是理解各自项目的业务逻辑做好笔记能根据案例和实际的业务场景举一反三。 下一篇认证服务