• 电商通用(二)


    9.网站首页高可用 nginx+lua

    导航:Lua脚本语言、nginx+Lua+redis实现缓存、nginx限流

    9.1Lua简介

    Lua是脚本语言,C语言编写,为了嵌入应用程序为程序提供灵活的扩展和定制功能。

    Lua特性:

    1.支持面向过程和函数式编程

    2.自动内存管理,只提供一种通用的类型的表,用它实现数组,哈希表,集合,对象

    3.语言内置模式匹配;闭包;函数也可以看作一个值;提供多线程

    4.通过闭包和table支持面向对象编程所需要的一些机制,比如数据抽象,虚函数,继承和重载。

    9.2nginx+lua+redis实现广告缓存

    1.nginx有什么用?

    1.负载均衡 2.反向代理 3.静态网页服务器

    正向代理:浏览器找到首页服务器的真实ip

    反向代理:找到nginx的ip,具体ip让nginx做

    2.tomcat与nginx,网页服务器区别是什么?

    1.从应用方面:tomcat一般是动态解析才会用到,支持jsp(html+java)解析,需要JDK。nginx一般是做静态html,负载均衡,反向代理,http服务器

    2.性能方面:tomcat并发500;nginx上万tps。

    9.3OpenResty

    OpenResty简介

    OpenResty(ngx_openresty)是一个基于nginx可伸缩的web应用服务器,web开发人员可以使用Lua脚本语言调动nginx支持的各种C以及Lua模块,更主要的是性能方面,OpenResty可以胜任10K乃至1000K以上并发连接响应的超高性能web应用系统。

    前置:

    需安装nginx、OpenResty在服务器

    代码实现

    数据库:changgou_business库下tb_ad表。

    一、广告预热实现

    实现:运维人员,脚本,每天凌晨2点。nginx发一个请求(http://127.0.0.1/ad_update?position=web_index_lb),让nginx来做。

    实现思路:用于查询数据库中的数据更新到redis中

    1.连接mysql:按照广告分类ID读取广告列表,转为json字符串

    2.连接redis,将广告列表json字符串存入redis

    定义请求:

    请求:/ad_update        参数:position        返回值:json

    1.编写vim /root/lua/ad_update.lua

    1. nginx.header.content_type="application/json;charset=utf8" #传递json数据
    2. local cjson = require("cjson") #引入json支持
    3. local mysql = require("resty.mysql") #引入mysql支持
    4. local uri_args = ngx.req.get_uri_args() #获取请求路径的参数 赋值给局部变量
    5. local position = uri_args["position"] #获取请求参数的特定字段 "position"
    6. local db = mysql:new() #开启mysql的新连接
    7. db:set_timeout(1000) #设置数据库连接超时时间
    8. local_props = {
    9. host = "192.168.200.128",
    10. port = 3306,
    11. database = "mall_business",
    12. user = "root",
    13. password = "root"
    14. }
    15. local res = db:connect(props) #基于四大参数连接mysql
    16. local select_sql = "select url, image from tb_ad status ='1' and position='"..position.."' and start_time<=NOW() AND end_time>=NOW()"
    17. res = db:query(select_sql) #传递执行的SQL语句
    18. db:close() #关闭连接池
    19. local redis = require("resty_redis") #引入redis模块
    20. local red = redis:new() #开启redis连接
    21. red:set_timeout(2000) #设置redis连接超时时间
    22. local ip = "192.168.200.128" #设置连接ip地址
    23. local port = 6379 #redis端口号
    24. red:connect(ip, port) #连接redis
    25. res:set("ad_"..position, cjson.encode(res)) #设置存入redis的内容
    26. res:close() #关闭redis连接
    27. ngx.say("{flag:true}") #返回信息

    2.使nginx执行lua脚本

    1. vim /usr/local/openresty/nginx/conf/nginx.conf
    2. #在 server 80 localhost中添加转发
    3. #添加广告
    4. location /ad_update{
    5. content_by_lua_file /root/lua/ad_update.lua;
    6. }

    注意:每次更改完需要重启nginx

    1. cd /usr/lcoal/openresty/nginx/sbin
    2. ./nginx -s reload

    二、读取广告实现

    用户访问首页,钩子方法里,请求(http://127.0.0.1/ad_read?position=web_index_lb)显示。

    实现思路:通过lua脚本直接从redis中获取数据即可。

    定义请求:请求/ad_read        参数:position        返回值:json

    1.编写vim /root/lua/ad_read.lua

    1. ngx.header.content_type="application/json;charset=utf8
    2. local uri_args = ngx.req.get_uri_qrgs();
    3. local position = uri_args["position"];
    4. local redis = require("resty.redis");
    5. local red = redis:new()
    6. red:set_timeout(2000)
    7. local ok, err = red:connect("192.168.200.128", 6379)
    8. local rescontent = red:get("ad_"..position) #查询redis的广告数据存入变量
    9. nginx.say(rescontent) #输出变量
    10. red:close()

    2.拦截请求,修改nginx读取lua脚本

    1. vim /usr/local/openresty/nginx/conf/nginx.conf
    2. locatoin /ad_read {
    3. content_by_lua_file /root/lua/ad_read.lua;
    4. }

    三、二级缓存读取实现

    问题:redis请求也上去了,我们直接采用多级缓存来减少下游系统的服务压力

    解决方案:先查openresty本地缓存,如果没有再查redis中的数据,再放入本地缓存10min

    1.修改root/lua目录下的ad_read文件

    1. #设置响应头
    2. ngx.header.content_type="application/json;charset=utf8
    3. #获取请求中的参数ID
    4. local uri_args = ngx.req.get_uri_qrgs();
    5. local position = uri_args["position"];
    6. local cache_ngx = ngx.shared.dis_cache; #开启本地缓存
    7. local adCache = cache_ngx:get('ad_cache_'..position); #在本地缓存中查询数据
    8. if(adCache==""or addCache==nil then #判断当前结果为空或无效
    9. local redis = require("resty.redis"); #引入redis库
    10. local red = redis:new() #创建redis对象
    11. red:set_timeout(2000) #设置超时时间
    12. local ok, err = red:connect("192.168.200.128", 6379) #连接
    13. local rescontent = red:get("ad_"..position) #获取key值
    14. nginx.say(rescontent) #输出返回信息
    15. red:close()
    16. cache_ngx:set('ad_cache_'..postition, rescontent, 10*60); #存入本地缓存10min
    17. else
    18. ngx.say(addCache)
    19. end

    2.修改nginx配置 vi /usr/local/openresty/mginx/conf/nginx.conf,http节点下添加配置

    1. #包含redis初始化模块
    2. lua_shared_dict dis_cache 5m; #共享内存开启

    9.4nginx限流

    首页并发量大,即使有了多级缓存,如果有大量恶意请求,也会对系统造成影响,就需要限流。

    nginx提供两种限流方式:

    1.控制速率

    2.控制并发链接数

    9.4.1控制速率

    采用漏桶算法。请求先进入漏桶中,漏桶以一定的速度出水,当水流速度大会直接溢出(访问频率超过接口响应速率),然后就拒绝。

    与令牌桶的区别:

    令牌桶控制的是进的速度。漏桶控制的出的速度。

    漏桶算法nginx实现配置:

    1. vim conf/nginx.conf
    2. #设置限流配置
    3. #binary_remote_addr:key,表示基于remote_addr(客户端ip)来做限流,binary_的目的是压缩内存占用量
    4. #zone:定义共享内存区来访问存储信息,myRateLimit内存区域名称,1m能存储16000ip地址
    5. #rate:用于设置最大访问速率,10r/s表示每秒最多处理10个请求
    6. limit_req_zone $binary_remote_addr zone=myRateLimit:10m rate=2r/s; #漏桶大小:10m
    7. server {
    8. listen 8081;
    9. server_name local;
    10. charset utf-8;
    11. location / {
    12. limit_req zone=myRateLimit;
    13. root html;
    14. index index.html index.html;
    15. }
    16. }

    9.4.2处理突发流量

    如果有正常流量突然增大,超出请求将被拒绝,可以用burst参数来解决该问题。

    burst表示超过设定的处理速率后能额外处理的请求数,当rate=2r/s时,将1s拆分成2份,即每500ms可处理一个请求。当burst=5时,同时有6个请求达到,nginx会处理第一个请求,剩余5个请求会放入队列,然后每隔500ms从队列中获取一个请求进行处理。若请求大于6,将拒绝多余请求,返回503.

    1. location / {
    2. limit_req zone=myRateLimit burst=5 nodelay;
    3. root html;
    4. index index.html index.html;
    5. }

    10.数据同步解决方案-canal

    定位:数据监控后端

    导航:数据监控微服务,首页广告缓存,商品上架索引库导入,下架索引库删除数据功能实现。

    10.1canal

    早期阿里杭州和美国双机房部署,存在跨域机房同步部署业务需求,实现方式主要是基于业务trigger获取增量变更。从10年开始,业务逐步尝试数据库日志解析获取增量变更进行同步,由此衍生出大量的数据库增量订阅和消费业务。

    MySql主备复制

    MySQL master将数据变更写入二进制日志binlog。

    MySQL slave将master的binlog events 拷贝到它的中继日志relay log

    MySQL slave重放relay log中事件,将数据变更反映它自己的数据

     cancal工作原理:

    1.canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议

    2.mysql master收到dump请求,开始推送binary log给slave(canal)

    3.canal解析binlog对象(byte流)

    10.1.2mysql开启binlog

    1.查看当前mysql是否开启binlog: show varibles like '%log_bin%'

    2.修改/etc/my.cnf需要开启binlog模式

    1. [mysqld]
    2. log-bin=mysql-bin
    3. binlog-format=ROW
    4. server_id=1

    3.进入mysql:mysql -h localhost -u root -p

    4.创建账号

    1. create user canal@'%' IDENTIFIED by 'canal';
    2. GRANT SELECT,REPLICATION SLAVE,REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%';
    3. FLUSH PRIVILEGES;

    10.1.3canal服务端安装配置

    百度

    10.2数据监控微服务

    当用户执行数据库的操作的时候,binlog日志会被canal捕获到并解析出数据。

    springboot与canal集成开源项目:https://github.com/chenqian56131/spring-boot-starter-canal

    10.3首页广告缓存更新

    需求:当tb_ad表数据变化时,更新redis中的广告数据。

    实现:数据监控微服务canal监听到tb_ad数据库发生变化时,通过rabbitMQ发送异步请求到http://127.0.0.1/ad_update?position=web_index_lb 执行lua(openresty)脚本

    代码实现:

    10.3.1发送消息到mq

    1.在rabbitmq管理后台创建队列ad_update_queue,用于接收广告更新通知

    2.引入rabbitmq依赖

    3.添加配置文件

    4.新增rabbitmq配置类

    5.测试重启canal,修改数据库,观察mq

    10.3.2从mq中提取消息执行更新

    mall_service_business工程中:

    1.添加依赖

    2.添加配置

    3.添加监听类

    代码:https://gitee.com/xuyu294636185/mall_parent.git

    10.3商品上架索引库导入数据

    需求:商品上架将商品的sku列表导入或更新索引库。

    实现:

    1.商品上架(spu.is_marktable 0变为1) --> canal监控 -->mq发送spuid

    2.在rabbitmq管理后台创建商品上架交换机(fanout订阅发布模式)

    3.spuid-》搜索服务-》feign商品服务获取skuList-》放入Es索引库。

    10.3.1发送消息到mq

    1.在rabbitmq后台创建交换机goods_up_exchange(fanout),创建队列search_add_queue绑定交换器goods_up_exchange,更新rabbitmq配置类。

    2.数据监控微服务新增canal.Listener.SpuListener代码。

    10.3.2索引库环境准备(elasticsearch的docker镜像)

    创建索引结构:新建mall_service_search_api模块,添加索引库实体类。

    10.3.3搜索微服务搭建

    1.创建mall_service_search模块,导依赖

    2.添加配置文件

    3.创建启动类

    4.配置RabbitMQConfig

    10.3.4视商品服务查询商品信息

    1.skuController新增方法

    2.mall_service_goods_api新增common依赖

    3.定义skuFeign接口

    10.3.5搜索微服务批量导入数据逻辑

    1.创建com.mall.search.dao包,新增ESManagerMapper接口

    2.创建com.mall.search.service包,创建接口ExManagerService

    10.3.6接收mq消息执行导入

    逻辑:canal监听到某个spu上架,发送到mq之后,搜索服务接受消息,将此spu对应的sku查出来,放到es索引库。

    mall_service_search工程创建com.mall.search.listener包,创建GoodsUpListener

    10.4商品下架索引库删除数据

    1.在数据监控微服务中监控tb_spu表的数据,当is_marketable为0时,表示商品下架,将spu的id发送到rabbitmq。

    2.在rabbitmq管理后台创建商品下架交换器(fanout)。使用分列模式的交换机是考虑商品下架会有很多种逻辑需要处理,索引库删除数据只是其中一项,另外还有删除商品详情页数等操作。

    3.搜索微服务从rabbitmq的队列中提取spu的id,通过elasticsearch的高级restAPI将相关的sku列表从索引库删除。

    10.4.1创建交换机与队列

    1.完成商品下架交换机的创建,队列的创建于绑定,将spuId发送消息到mq

    2.将rabbitmqConfig复制到搜索服务

    10.4.2canal监听下架

    修改mall_canal的SpuListener的SpuUpdate方法,添加下架判断发送消息逻辑

    10.4.3根据spuId删除索引数据

    ESManagerService新增删除方法。

    10.4.4接收mq消息,执行索引库删除  

    从rabbitmq中提取消息,调动根据spuId删除索引库数据的方法mall_service_search新增监听类

    代码:https://gitee.com/xuyu294636185/mall_parent.git

    11.商品搜索-elastaicSearch

    导航:搜索关键字查询,条件筛选,规格过滤,价格区间搜索,分页查询,排序查询,高亮查询

    11.1关键字查询-构建搜索条件

    1. public class SearchServiceImpl implements SearchService {
    2. @Autowired
    3. ElasticsearchTemplate elasticsearchTemplate;
    4. @Override
    5. public Map search(Map<String, String> searchMap) {
    6. NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
    7. BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
    8. if (searchMap != null) {
    9. //多个搜索条件 bool
    10. if (StringUtils.isNotEmpty(searchMap.get("keywords"))) {
    11. MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("name", searchMap.get("keywords"));
    12. boolQueryBuilder.must(matchQueryBuilder);
    13. }
    14. }
    15. nativeSearchQueryBuilder.withQuery(boolQueryBuilder);
    16. /**
    17. * nativeSearchQueryBuilder 能承接一些搜索的条件
    18. * SkuInfo 实体类对象
    19. * SearchResultMapper 结果集,对象 映射
    20. */
    21. AggregatedPage<SkuInfo> skuInfos = elasticsearchTemplate.queryForPage(nativeSearchQueryBuilder.build(), SkuInfo.class, new SearchResultMapper() {
    22. //把搜索结果和对象映射
    23. @Override
    24. public AggregatedPage mapResults(SearchResponse searchResponse, Class aClass, Pageable pageable) {
    25. List list = new ArrayList<>();
    26. SearchHits hits = searchResponse.getHits(); //大Hits ,含有页数信息的外层结构
    27. //总数
    28. long totalHits = hits.getTotalHits();
    29. SearchHit[] hits1 = hits.getHits();
    30. for (SearchHit hit : hits1) {
    31. //hit --> SkuInfo对象
    32. String sourceAsString = hit.getSourceAsString(); //拿到{id:123,name:xxx,price} json串
    33. SkuInfo skuInfo = JSON.parseObject(sourceAsString, SkuInfo.class);
    34. list.add((T) skuInfo);
    35. }
    36. return new AggregatedPageImpl<>(list, pageable, totalHits, searchResponse.getAggregations());
    37. }
    38. });
    39. Map resultMap = new HashMap();
    40. //总记录数
    41. resultMap.put("total", skuInfos.getTotalElements());
    42. //总页数
    43. resultMap.put("totalPages", skuInfos.getTotalPages());
    44. //数据集合
    45. resultMap.put("rows", skuInfos.getContent());
    46. return null;
    47. }
    48. }

    11.2各种查询

     11.2.1品牌过滤查询

    需求:search.mall.com/search?keywords=包&brand=prada

    1. //品牌查询
    2. if(StringUtils.isNotEmpty(searchMap.get("brand"))){
    3. //brandName在es中类型是keyword 需要使用trerm查询
    4. TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("brandName", searchMap.get("brand"));
    5. //term查询一般放在filter中
    6. boolQueryBuilder.filter(termQueryBuilder);
    7. }

    11.2.2品牌聚合查询

    需求:查看静态页面,品牌的显示要根据搜索结果不同展示。(在es中按照beandName进行聚合查询)

    1. //品牌查询
    2. if(StringUtils.isNotEmpty(searchMap.get("brand"))){
    3. //brandName在es中类型是keyword 需要使用trerm查询
    4. TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("brandName", searchMap.get("brand"));
    5. //term查询一般放在filter中
    6. boolQueryBuilder.filter(termQueryBuilder);
    7. }
    8. //聚合 品牌
    9. String skuBrand = "skuBrand";
    10. TermsAggregationBuilder brandTerms = AggregationBuilders.terms(skuBrand).field("brandName");
    11. nativeSearchQueryBuilder.addAggregation(brandTerms);
    12. //把品牌聚合的结果返回前端
    13. StringTerms brandStringTerms = (StringTerms) skuInfos.getAggregation(skuBrand);
    14. List brandList = brandStringTerms.getBuckets().stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());
    15. resultMap.put("brandList",brandList);

    11.2.3按照规格过滤

    需求:前端发规格查询,后端接收到数据可以根据spec_来区分是否是规格,如果以spec_xxx开始的数据则为规格数据,需要根据指定规格找到信息。

    http://localhost:9009/search?keywords=包&spec_颜色=黑色&spec_二手成都=九新

    上图是规格的索引存储格式,真实数据在“specMap.二手成都.keyword”中,所以找数据也是按照如下格式去找。

    1. //规格查询 spec_有多个需要遍历
    2. for (String key : searchMap.keySet()) {
    3. if(key.startsWith("spec_")){
    4. String value = searchMap.get(key).replace("%2B","+"); //url特殊字符转换
    5. TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("searchMap." + key.substring(5) + ".keyword", value);
    6. boolQueryBuilder.filter(termQueryBuilder);
    7. }
    8. }

    11.2.4按照规格聚合

    text类型不能进行集合和排序,keyword可以进行聚合和排序

    11.2.5价格区间查询

    需求:前端传入后台price=0-500或者price=500-1000依次类推,最后一个是price=3000,后台可以根据-分割,如果分割得到的结果最多有2个,第1个表示x

    1. if(StringUtils.isNotEmpty(searchMap.get("price"))){
    2. String price = searchMap.get("price");
    3. String[] split = price.split("-");
    4. if(split.length==2){
    5. //price=0-5000
    6. RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("price").gte(split[0]).lte(split[1]);
    7. boolQueryBuilder.filter(rangeQueryBuilder);
    8. }else{
    9. //price=5001
    10. RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("price").gte(split[0]);
    11. boolQueryBuilder.filter(rangeQueryBuilder);
    12. }
    13. }

    11.2.6分页

    需求:pageNum=2&pageSize=20

    1. //分页
    2. String pageNum = searchMap.get("pageNum");
    3. String pageSize = searchMap.get("pageSize");
    4. if(StringUtils.isEmpty("pageNum")){
    5. pageNum = "1";
    6. }
    7. if(StringUtils.isEmpty("pageSize")){
    8. pageSize = "20";
    9. }
    10. PageRequest pageRequest = PageRequest.of(Integer.parseInt(pageNum) - 1, Integer.parseInt(pageSize));
    11. nativeSearchQueryBuilder.withPageable(pageRequest);

    11.2.7排序

    排序总共有根据价格排序,根据评价排序,根据新品排序,根据销量排序,想要排序只需要告知排序的域以及排序方式即可实现。

    需求:sortField=price&sortRule=Desc

    1. //排序
    2. String sortField = searchMap.get("sortField");
    3. String sortRule = searchMap.get("sortRule");
    4. if (StringUtils.isNotEmpty(sortField) && StringUtils.isNotEmpty(sortRule)) {
    5. if(sortRule.equals("ASC")){
    6. nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(sortField).order(SortOrder.ASC));
    7. }else{
    8. nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(sortField).order(SortOrder.DESC));
    9. }
    10. }

    11.2.8高亮

    1. //设置高亮
    2. HighlightBuilder field = new HighlightBuilder().field("name");
    3. //前签
    4. field.preTags("");
    5. //后签
    6. field.postTags("");
    7. nativeSearchQueryBuilder.withHighlightBuilder(field);
    8. //获取高亮,设置到name属性中
    9. Map<String, HighlightField> highlightFields = hit.getHighlightFields();
    10. HighlightField highlightField = highlightFields.get("name");
    11. Text[] fragments = highlightField.getFragments();
    12. String realName = "";
    13. for (Text fragment : fragments) {
    14. realName+=fragment; //jdk底层
    15. }
    16. skuInfo.setName(realName);
    17. list.add((T) skuInfo);

    代码:https://gitee.com/xuyu294636185/mall_parent.git

    12.Thymeleaf

    导航:Thymeleaf入门,语法和标签,搜索页面渲染,商品详情页静态化功能实现。

    12.1Thymeleaf简介

    12.1.1动态页面

    通过执行asp,php等程序生成客户端网页代码的网页。

    12.1.2静态页面

    12.1.3为什么需要动态页面静态化

    1.搜索引擎的优化

    SEO优化,增加搜索访问量

    2.提高程序性能

    模板+数据=文本

    12.1.4Thtmeleaf介绍

    概念:XML/XHTML/HTML5模板引擎

    其他模板引擎:Velocity,FreeMarker,jsp

    使用:springboot内部支持

    特点:开箱即用,Thymeleaf允许您处理六种模板,每种模板成为模板模式:

    XML,有效的XML,XHTML,有效的XHTML,HTML5,旧版HTML5

    12.2Springboot整合thymeleaf

    使用springboot整合thymeleaf可以减少代码量。

    1.创建springboot-thymeleaf工程。

    1. <parent>
    2. <groupId>org.springframework.bootgroupId>
    3. <artifactId>spring-boot-starter-parentartifactId>
    4. <version>2.1.4.RELEASEversion>
    5. parent>
    6. <dependencies>
    7. <dependency>
    8. <groupId>org.springframework.bootgroupId>
    9. <artifactId>spring-boot-starter-webartifactId>
    10. dependency>
    11. <dependency>
    12. <groupId>org.springframework.bootgroupId>
    13. <artifactId>spring-boot-starter-thymeleafartifactId>
    14. dependency>
    15. dependencies>
    1. @Controller
    2. @RequestMapping("test")
    3. public class TeatController {
    4. //model放值
    5. //view 放返回demo页面
    6. @GetMapping("demo")
    7. public String demo(Model model){
    8. //从数据库获取到真实的值
    9. model.addAttribute("hello","hello");
    10. return "demo";
    11. }
    12. }
    1. resource下创建template包存放模板:demo.html
    2. html>
    3. <html xmlns:th="http://www.thymeleaf.org">
    4. <head>
    5. <title>Thymeleaf 入门title>
    6. <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    7. head>
    8. <body>
    9. <p th:text="${hello}">p>
    10. body>
    11. html>

    12.3thymeleaf基本语法

    百度

    12.4搜索页面渲染

    搜索页面要显示的内容主要分为:1.搜索的数据结果2.筛选出的数据搜索条件3.用户已经勾选的数据条件

    12.4.1搜索工程搭建

    1.mall_service_search导包

    2.静态资源导入resource

    3.更改配置文件:spring.thymeleaf.cache:false

    4.修改Controller类RestController为@Controller,search方法添加@ResponseBody

    5.搜索页面修改参数渲染

    1. xmlns:th="http://thymeleaf.org">
    2. //动态替换搜索参数
    3. <li class="active">
    4. <span th:text="${searchMap.keyword}">span>
    5. li>
    6. //条件参数判断替换
    7. <li class="with-x" th:if="${#maps.containsKey(searchMap,'brand')}">
    8. 品牌:<span th:text="${searchMap.brand}">span>
    9. <a th:href="@{${#strings.replace(url,'&brand='+searcMap.brand,'')}}">xa>
    10. li>

    12.4.2品牌信息显示

    1. //显示聚合查询后的品牌图标
    2. class="type-wrap logo" th:unless="${#maps.contaionsKey(searchMap,'brand')}">
    3. <div class="fl key brand">品牌div>
    4. <div class="value logos">
    5. <ul class="logo-list">
    6. <li th:each="brand,brandSate:${result.brandList}">
    7. <a th:text="${brand}" th:href="@{${url}(brand=${brand})}">a>
    8. li>
    9. ul>
    10. div>
  • 12.4.3规格信息数据转换

    1. /**
    2. * 规格转换
    3. * "{'颜色':'黄色','版本':'4+64'}"
    4. * "{'颜色':'蓝色','版本':'4+64'}"
    5. * "{'颜色':'黄色','版本':'6+128'}"
    6. * "{'颜色':'蓝色','版本':'6+128'}"
    7. * *******************
    8. * 颜色:黄色,蓝色
    9. * 版本:4+64,6+128
    10. */
    11. public Map<String, Set<String>> formatSpec(List<String> specList){
    12. HashMap<String, Set<String>> resultMap = new HashMap<>();
    13. for (String specStr : specList) {
    14. //specStr:"{'颜色':'黄色','版本':'4+64'}"
    15. Map<String,String> specMap = JSON.parseObject(specStr, Map.class); //颜色:黄色 版本:4+64
    16. //遍历Map
    17. for (String key : specMap.keySet()) {
    18. //key:颜色 value:蓝色
    19. Set<String> valueSet = resultMap.get(key);
    20. if(valueSet == null){
    21. valueSet = new HashSet<>();
    22. }
    23. valueSet.add(specMap.get(key));
    24. resultMap.put(key,valueSet);
    25. }
    26. }
    27. return resultMap;
    28. }
    1. "type-wrap" th:each="spec,specStat:${result.specList}" th:unless="${#maps.containsKey(searchMap,'spec_'+spec.key)}">
    2. "fl key" th:text="${spec.key}">
  • "fl value">
    • "type-list">
  • "op,opstat:${spec.value}">
  • "fl ext">
  • 12.5条件搜索实现:

    需求:用户搜索:拼接url/search/list?keywords=包包

    点击新规格:拼接url/search/list?keywords=包包&spec_颜色=红色

     后端代码实现:

    1. @GetMapping("/list")
    2. public String List(@RequestParam Map<String, String> searchMap, Model model){
    3. handleSearchMap(searchMap);
    4. //搜索结果
    5. Map resultMap = searchService.search(searchMap);
    6. model.addAttribute("result",resultMap);
    7. //用户搜索条件
    8. model.addAttribute("searchMap",searchMap);
    9. //记录url 记录这次的搜索条件,前端不会丢失上一次的条件
    10. StringBuilder url = new StringBuilder("/search/list");
    11. //判断有没有搜索条件
    12. if(searchMap!=null && searchMap.size()>0){
    13. url.append("?");
    14. for (String key : searchMap.keySet()) {
    15. //排除一些不需要的搜索条件
    16. if(!key.equals("sortField")&&!key.equals("sortRule")&&!key.equals("pageNum")&&!key.equals("pageSize")) {
    17. //searchMap keywords=包包 spec_颜色=黑色
    18. url.append(key).append("=").append(searchMap.get(key)).append("&");
    19. }
    20. }
    21. String urlString = url.toString();
    22. urlString.substring(0,urlString.length()-1);//去掉最后的&字符
    23. model.addAttribute("url",urlString);
    24. }else{
    25. model.addAttribute("url",url);
    26. }
    27. return "search";
    28. }

    前端url拼接跳转:用户点击相应品牌,规格,价格,跳转请求后端接口。

    12.6移除搜索条件

    需求:用户点击x去除刚才的搜索条件

    1. <li class="with-x" th:if="${#maps.containsKey(searchMap,'brand')}">
    2. 品牌:<span th:text="${searchMap.brand}">span>
    3. <a th:href="@{${#strings.replace(url,'&brand='+searchMap.brand,'')}}">xa>
    4. li>
    5. <li class="with-x" th:if="${#maps.containsKey(searchMap,'price')}">
    6. 价格:<span th:text="${searchMap.price}">span>
    7. <a th:href="@{${#strings.replace(url,'&price='+searchMap.price,'')}}">xa>
    8. li>
    9. <li class="with-x" th:each="sm,mapStat:${searchMap}" th:if="${#strings.startWith(sm.key,'spec_')}">
    10. <span th:text="${#strings.replace(sm.key,'sepc_','')}">span> : <span th:text="${#strings.replace(sm.value,'%2B','')}">span>
    11. <a th:href="@{${#strings.replace(url,'&'+sm.key+'='+sm.value,'')}}">xa>
    12. li>

    12.7排序

    需求:用户点击排序字段,返回排好序的内容

    修改search.html页面:

    12.8分页

    基于thymeleaf前后端实现分页

    代码:https://gitee.com/xuyu294636185/mall_parent.git

    13.商品详情页

    流程:

    1.商品上架-》商品服务发送spuid->mq

    2.mq->静态页微服务

    3.静态页服务->调用商品服务获取spu->生成静态页面

    13.1商品静态化微服务创建

    该微服务只用于生成视商品静态页。

    1.创建静态页服务mall_service_page

    2.依赖

    3.配置文件

    13.2生成静态页

    13.2.1需求分析

    页面发送请求后,传递要生成的静态页的商品SpuId,后台controller接受请求,调用thymeleaf的原生API生成商品静态页。

     上图是生成的商品详情页,从图片上可看出需要查询SPU的3个分类作为“面包屑”显示,同时还需要查询SKU和SPU信息。

    13.2.2Feign创建

    需求:查询3部分数据(分类、spu、sku) 资源/静态原型/前台/item.html

    1.在goods-api中创建好3个feign。

    2.静态页面生成代码

    将item.html放到template下。

    page模块service层com.mall.page.service

    实现com.mall.page.service.impl

    1. public class PageServiceImpl implements PageService {
    2. @Autowired
    3. TemplateEngine templateEngine;
    4. @Value("${pagepath}")
    5. String pagepath;
    6. @Override
    7. public void generateHtml(String spuId) {
    8. //1构建上下文,包含模板+数据
    9. Context context = new Context();
    10. Map<String, Object> data = getData(spuId);
    11. context.setVariables(data);
    12. //2.处理文件
    13. File dir = new File(pagepath); //文件夹 d:/item
    14. //如果文件夹不存在,创建
    15. if (!dir.exists()) {
    16. dir.mkdirs();
    17. }
    18. //d://item/spuid.html
    19. File file = new File(pagepath + "/" + spuId + ".html");
    20. Writer writer = null;
    21. try {
    22. writer = new FileWriter(file);
    23. templateEngine.process("item", context, writer);
    24. } catch (IOException e) {
    25. e.printStackTrace();
    26. } finally {
    27. //关闭流
    28. try {
    29. writer.close();
    30. } catch (IOException e) {
    31. e.printStackTrace();
    32. }
    33. }
    34. }
    35. @Autowired
    36. SpuFeign spuFeign;
    37. @Autowired
    38. SkuFeign skuFeign;
    39. @Autowired
    40. CategoryFeign categoryFeign;
    41. //获取数据方法
    42. public Map<String, Object> getData(String spuId) {
    43. HashMap<String, Object> resultMap = new HashMap<>();
    44. //1.spu
    45. Spu spu = spuFeign.findSpuById(spuId).getData();
    46. resultMap.put("spu", spu);
    47. //4.imageList
    48. String images = spu.getImages();
    49. if (StringUtils.isNotEmpty(images)) {
    50. String[] imageList = images.split(",");
    51. resultMap.put("imageList", imageList);
    52. }
    53. //5.specificationList
    54. String specs = spu.getSpec_items();
    55. if (StringUtils.isNotEmpty(specs)) {
    56. Map specMap = JSON.parseObject(specs, Map.class);
    57. resultMap.put("specificationList", specMap);
    58. }
    59. //1.sku
    60. List<Sku> skuList = skuFeign.findSkuListBySpuId(spuId);
    61. resultMap.put("skuList", skuList);
    62. //3.category
    63. Integer category1_id = spu.getCategory1_id();
    64. Integer category2_id = spu.getCategory2_id();
    65. Integer category3_id = spu.getCategory3_id();
    66. Category category1 = categoryFeign.findById(category1_id).getData();
    67. Category category2 = categoryFeign.findById(category2_id).getData();
    68. Category category3 = categoryFeign.findById(category3_id).getData();
    69. resultMap.put("category1", category1);
    70. resultMap.put("category2", category2);
    71. resultMap.put("category3", category3);
    72. return resultMap;
    73. }
    74. }

    3.静态页服务监听

    config.mall.page.config粘贴canal服务中的配置

    1. //创建静态页面的队列
    2. public static final String PAGE_CRATE_QUEUE = "page_create_queue";
    3. //定义静态页面的队列
    4. @Bean(PAGE_CRATE_QUEUE)
    5. public Queue PAGE_CREATE_QUEUE(){
    6. return new Queue(PAGE_CRATE_QUEUE);
    7. }
    8. //创建静态页面的队列和上架交换机关系构建
    9. @Bean
    10. public Binding GOODS_UP_EXCHANGE_PAGE_CREATE_QUEUE(
    11. @Qualifier(PAGE_CRATE_QUEUE)Queue queue,
    12. @Qualifier(GOODS_UP_EXCHANGE)Exchange exchange
    13. ){
    14. return BindingBuilder.bind(queue).to(exchange).with("").noargs();
    15. }

    com.mall.page.listener

    1. @Component
    2. public class PageListener {
    3. @Autowired
    4. PageService pageService;
    5. @RabbitListener(queues = RabbitMQConfig.PAGE_CRATE_QUEUE)
    6. public void receiveMsg(String spuId){
    7. System.out.println("监听到商品上架了,生成静态页面:"+spuId);
    8. pageService.generateHtml(spuId);
    9. }
    10. }

    代码:https://gitee.com/xuyu294636185/mall_parent.git

  • 相关阅读:
    java学习--day22(进程&线程)
    Jumpserver安全一窥:Sep系列漏洞深度解析
    Hbase2.4.11安装
    AP51656 PWM和线性调光 LED车灯电源驱动IC 兼容替代PT4115 PT4205
    计算机视觉 CV 八股分享 [自用](更新中......)
    基于多模态视觉模型和图文向量模型的工业图像知识库研究与应用
    Jmeter的接口自动化测试
    Linux使用tab补全时报没有空间的错
    sgu 176 Flow construction (有源汇的上下界最小流)
    AutoJs学习-MC我的世界自动钓鱼
  • 原文地址:https://blog.csdn.net/xy294636185/article/details/125224561