电商通用(二)

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

nginx.header.content_type="application/json;charset=utf8"  #传递json数据
local cjson = require("cjson")      #引入json支持
local mysql = require("resty.mysql")  #引入mysql支持
local uri_args = ngx.req.get_uri_args()  #获取请求路径的参数 赋值给局部变量
local position = uri_args["position"]    #获取请求参数的特定字段 "position"

local db = mysql:new()  #开启mysql的新连接
db:set_timeout(1000)    #设置数据库连接超时时间
local_props = {
    host = "192.168.200.128",
    port = 3306,
    database = "mall_business",
    user = "root",
    password = "root"
}

local res = db:connect(props)   #基于四大参数连接mysql
local select_sql = "select url, image from tb_ad status ='1' and position='"..position.."' and start_time<=NOW() AND end_time>=NOW()"
res = db:query(select_sql)   #传递执行的SQL语句
db:close()    #关闭连接池 

local redis = require("resty_redis")   #引入redis模块
local red = redis:new()               #开启redis连接
red:set_timeout(2000)      #设置redis连接超时时间
local ip = "192.168.200.128"   #设置连接ip地址
local port = 6379              #redis端口号
red:connect(ip, port)          #连接redis

res:set("ad_"..position, cjson.encode(res))    #设置存入redis的内容
res:close()                    #关闭redis连接     

ngx.say("{flag:true}")   #返回信息

2.使nginx执行lua脚本

vim /usr/local/openresty/nginx/conf/nginx.conf

#在 server 80 localhost中添加转发
#添加广告
location /ad_update{
    content_by_lua_file /root/lua/ad_update.lua;
}

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

cd /usr/lcoal/openresty/nginx/sbin
./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

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

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

vim /usr/local/openresty/nginx/conf/nginx.conf

locatoin /ad_read {
    content_by_lua_file /root/lua/ad_read.lua;
}

三、二级缓存读取实现

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

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

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

#设置响应头
ngx.header.content_type="application/json;charset=utf8
#获取请求中的参数ID
local uri_args = ngx.req.get_uri_qrgs();
local position = uri_args["position"];

local cache_ngx = ngx.shared.dis_cache;   #开启本地缓存
local adCache = cache_ngx:get('ad_cache_'..position);   #在本地缓存中查询数据
if(adCache==""or addCache==nil then    #判断当前结果为空或无效
    local redis = require("resty.redis"); #引入redis库
    local red = redis:new() #创建redis对象
    red:set_timeout(2000) #设置超时时间
    local ok, err = red:connect("192.168.200.128", 6379) #连接
    local rescontent = red:get("ad_"..position) #获取key值
    nginx.say(rescontent) #输出返回信息
    red:close()

    cache_ngx:set('ad_cache_'..postition, rescontent, 10*60); #存入本地缓存10min
else
    ngx.say(addCache)
end

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

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

9.4nginx限流

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

nginx提供两种限流方式:

1.控制速率

2.控制并发链接数

9.4.1控制速率

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

与令牌桶的区别:

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

漏桶算法nginx实现配置:

vim conf/nginx.conf

#设置限流配置
#binary_remote_addr:key,表示基于remote_addr(客户端ip)来做限流,binary_的目的是压缩内存占用量
#zone:定义共享内存区来访问存储信息,myRateLimit内存区域名称,1m能存储16000ip地址
#rate:用于设置最大访问速率,10r/s表示每秒最多处理10个请求
limit_req_zone $binary_remote_addr zone=myRateLimit:10m rate=2r/s; #漏桶大小:10m

server {
    listen    8081;
    server_name    local;
    charset  utf-8;
    location / {
        limit_req zone=myRateLimit;
        root    html;
        index    index.html    index.html;
    }
}

9.4.2处理突发流量

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

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

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

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模式

[mysqld]
log-bin=mysql-bin
binlog-format=ROW
server_id=1

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

4.创建账号

create user canal@'%' IDENTIFIED by 'canal';
GRANT SELECT,REPLICATION SLAVE,REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%';
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关键字查询-构建搜索条件

public class SearchServiceImpl implements SearchService {

    @Autowired
    ElasticsearchTemplate elasticsearchTemplate;

    @Override
    public Map search(Map<String, String> searchMap) {
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        if (searchMap != null) {
            //多个搜索条件 bool
            if (StringUtils.isNotEmpty(searchMap.get("keywords"))) {
                MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("name", searchMap.get("keywords"));
                boolQueryBuilder.must(matchQueryBuilder);
            }
        }

        nativeSearchQueryBuilder.withQuery(boolQueryBuilder);

        /**
         * nativeSearchQueryBuilder 能承接一些搜索的条件
         * SkuInfo 实体类对象
         * SearchResultMapper 结果集,对象 映射
         */
        AggregatedPage<SkuInfo> skuInfos = elasticsearchTemplate.queryForPage(nativeSearchQueryBuilder.build(), SkuInfo.class, new SearchResultMapper() {
            //把搜索结果和对象映射
            @Override
            public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse, Class<T> aClass, Pageable pageable) {
                List<T> list = new ArrayList<>();
                SearchHits hits = searchResponse.getHits();  //大Hits ,含有页数信息的外层结构
                //总数
                long totalHits = hits.getTotalHits();

                SearchHit[] hits1 = hits.getHits();
                for (SearchHit hit : hits1) {
                    //hit --> SkuInfo对象
                    String sourceAsString = hit.getSourceAsString();  //拿到{id:123,name:xxx,price} json串
                    SkuInfo skuInfo = JSON.parseObject(sourceAsString, SkuInfo.class);
                    list.add((T) skuInfo);
                }
                return new AggregatedPageImpl<>(list, pageable, totalHits, searchResponse.getAggregations());
            }
        });
        Map resultMap = new HashMap();
        //总记录数
        resultMap.put("total", skuInfos.getTotalElements());
        //总页数
        resultMap.put("totalPages", skuInfos.getTotalPages());
        //数据集合
        resultMap.put("rows", skuInfos.getContent());
        return null;
    }
}

11.2各种查询

 11.2.1品牌过滤查询

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

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

11.2.2品牌聚合查询

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

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

        //聚合 品牌
        String skuBrand = "skuBrand";
        TermsAggregationBuilder brandTerms = AggregationBuilders.terms(skuBrand).field("brandName");
        nativeSearchQueryBuilder.addAggregation(brandTerms);

        //把品牌聚合的结果返回前端
        StringTerms brandStringTerms = (StringTerms) skuInfos.getAggregation(skuBrand);
        List<String> brandList = brandStringTerms.getBuckets().stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());
        resultMap.put("brandList",brandList);

11.2.3按照规格过滤

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

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

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

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

11.2.4按照规格聚合

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

11.2.5价格区间查询

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

            if(StringUtils.isNotEmpty(searchMap.get("price"))){
                String price = searchMap.get("price");
                String[] split = price.split("-");
                if(split.length==2){
                    //price=0-5000
                    RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("price").gte(split[0]).lte(split[1]);
                    boolQueryBuilder.filter(rangeQueryBuilder);
                }else{
                    //price=5001
                    RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("price").gte(split[0]);
                    boolQueryBuilder.filter(rangeQueryBuilder);
                }
            }

11.2.6分页

需求:pageNum=2&pageSize=20

        //分页
        String pageNum = searchMap.get("pageNum");
        String pageSize = searchMap.get("pageSize");
        if(StringUtils.isEmpty("pageNum")){
            pageNum = "1";
        }
        if(StringUtils.isEmpty("pageSize")){
            pageSize = "20";
        }
        PageRequest pageRequest = PageRequest.of(Integer.parseInt(pageNum) - 1, Integer.parseInt(pageSize));
        nativeSearchQueryBuilder.withPageable(pageRequest);

11.2.7排序

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

需求:sortField=price&sortRule=Desc

        //排序
        String sortField = searchMap.get("sortField");
        String sortRule = searchMap.get("sortRule");
        if (StringUtils.isNotEmpty(sortField) && StringUtils.isNotEmpty(sortRule)) {
            if(sortRule.equals("ASC")){
                nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(sortField).order(SortOrder.ASC));
            }else{
                nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(sortField).order(SortOrder.DESC));
            }
        }

11.2.8高亮

        //设置高亮
        HighlightBuilder field = new HighlightBuilder().field("name");
        //前签
        field.preTags("<span style='color:red'>");
        //后签
        field.postTags("</span>");
        nativeSearchQueryBuilder.withHighlightBuilder(field);                   

         //获取高亮,设置到name属性中
         Map<String, HighlightField> highlightFields = hit.getHighlightFields();
         HighlightField highlightField = highlightFields.get("name");
         Text[] fragments = highlightField.getFragments();
         String realName = "";
         for (Text fragment : fragments) {
             realName+=fragment;  //jdk底层
         }
         skuInfo.setName(realName);
         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工程。

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.4.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>
@Controller
@RequestMapping("test")
public class TeatController {

    //model放值
    //view 放返回demo页面
    @GetMapping("demo")
    public String demo(Model model){
        //从数据库获取到真实的值
        model.addAttribute("hello","hello");
        return "demo";
    }
}
resource下创建template包存放模板:demo.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
    <title>Thymeleaf 入门</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>

<body>
<!--输出hello数据-->
<p th:text="${hello}"></p>
</body>
</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.搜索页面修改参数渲染

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

12.4.2品牌信息显示

//显示聚合查询后的品牌图标
<div class="type-wrap logo" th:unless="${#maps.contaionsKey(searchMap,'brand')}">
    <div class="fl key brand">品牌</div>
    <div class="value logos">
        <ul class="logo-list">
            <li th:each="brand,brandSate:${result.brandList}">
                <a th:text="${brand}" th:href="@{${url}(brand=${brand})}"></a>
            </li>
        </ul>
    </div>
</div>

12.4.3规格信息数据转换

    /**
     * 规格转换
     * "{'颜色':'黄色','版本':'4+64'}"
     * "{'颜色':'蓝色','版本':'4+64'}"
     * "{'颜色':'黄色','版本':'6+128'}"
     * "{'颜色':'蓝色','版本':'6+128'}"
     * *******************
     * 颜色:黄色,蓝色
     * 版本:4+64,6+128
     */
    public Map<String, Set<String>> formatSpec(List<String> specList){
        HashMap<String, Set<String>> resultMap = new HashMap<>();
        for (String specStr : specList) {
            //specStr:"{'颜色':'黄色','版本':'4+64'}"
            Map<String,String> specMap = JSON.parseObject(specStr, Map.class);  //颜色:黄色 版本:4+64
            //遍历Map
            for (String key : specMap.keySet()) {
                //key:颜色 value:蓝色
                Set<String> valueSet = resultMap.get(key);
                if(valueSet == null){
                    valueSet = new HashSet<>();
                }
                valueSet.add(specMap.get(key));
                resultMap.put(key,valueSet);
            }
        }
        return resultMap;
    }
<div class="type-wrap" th:each="spec,specStat:${result.specList}" th:unless="${#maps.containsKey(searchMap,'spec_'+spec.key)}">
    <div class="fl key" th:text="${spec.key}">
    </div>
    <div class="fl value">
        <ul class="type-list">
            <li th:each="op,opstat:${spec.value}">
                <a th:text="${op}" th:href="@{${url}('spec_'+${spec.key}=${op})}"></a>
            </li>
        </ul>
    </div>
    <div class="fl ext"></div>
</div>

12.5条件搜索实现:

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

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

 后端代码实现:

    @GetMapping("/list")
    public String List(@RequestParam Map<String, String> searchMap, Model model){
        handleSearchMap(searchMap);
        //搜索结果
        Map resultMap = searchService.search(searchMap);
        model.addAttribute("result",resultMap);
        //用户搜索条件
        model.addAttribute("searchMap",searchMap);
        //记录url 记录这次的搜索条件,前端不会丢失上一次的条件
        StringBuilder url = new StringBuilder("/search/list");
        //判断有没有搜索条件
        if(searchMap!=null && searchMap.size()>0){
            url.append("?");
            for (String key : searchMap.keySet()) {
                //排除一些不需要的搜索条件
                if(!key.equals("sortField")&&!key.equals("sortRule")&&!key.equals("pageNum")&&!key.equals("pageSize")) {
                    //searchMap  keywords=包包  spec_颜色=黑色
                    url.append(key).append("=").append(searchMap.get(key)).append("&");
                }
            }
            String urlString = url.toString();
            urlString.substring(0,urlString.length()-1);//去掉最后的&字符
            model.addAttribute("url",urlString);
        }else{
            model.addAttribute("url",url);
        }
        return "search";
    }

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

<a th:text="${brand}" th:href="@{${url}(brand=${brand})}"></a>
<a th:text="${op}" th:href="@{${url}('spec_'=${spec.key}=${op})}"></a>
<a th:text="0-500元" th:href="@{${url}(price='0-500')}"></a>

12.6移除搜索条件

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

<li class="with-x" th:if="${#maps.containsKey(searchMap,'brand')}">
    品牌:<span th:text="${searchMap.brand}"></span>
    <a th:href="@{${#strings.replace(url,'&brand='+searchMap.brand,'')}}">x</a>
</li>
<li class="with-x" th:if="${#maps.containsKey(searchMap,'price')}">
    价格:<span th:text="${searchMap.price}"></span>
    <a th:href="@{${#strings.replace(url,'&price='+searchMap.price,'')}}">x</a>
</li>
<!--规格 spec_大小=大&spec_使用人群=青年-->
<li class="with-x" th:each="sm,mapStat:${searchMap}" th:if="${#strings.startWith(sm.key,'spec_')}">
    <span th:text="${#strings.replace(sm.key,'sepc_','')}"></span> : <span th:text="${#strings.replace(sm.value,'%2B','')}"></span>
    <a th:href="@{${#strings.replace(url,'&'+sm.key+'='+sm.value,'')}}">x</a>
</li>

12.7排序

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

修改search.html页面:

<li>
    <a th:href="@{${url}(sortRule='ASC',sortField='price')}">价格👆</a>
</li>

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

public class PageServiceImpl implements PageService {

    @Autowired
    TemplateEngine templateEngine;

    @Value("${pagepath}")
    String pagepath;

    @Override
    public void generateHtml(String spuId) {
        //1构建上下文,包含模板+数据
        Context context = new Context();
        Map<String, Object> data = getData(spuId);
        context.setVariables(data);

        //2.处理文件
        File dir = new File(pagepath);  //文件夹 d:/item
        //如果文件夹不存在,创建
        if (!dir.exists()) {
            dir.mkdirs();
        }

        //d://item/spuid.html
        File file = new File(pagepath + "/" + spuId + ".html");

        Writer writer = null;
        try {
            writer = new FileWriter(file);
            templateEngine.process("item", context, writer);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //关闭流
            try {
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    @Autowired
    SpuFeign spuFeign;

    @Autowired
    SkuFeign skuFeign;

    @Autowired
    CategoryFeign categoryFeign;

    //获取数据方法
    public Map<String, Object> getData(String spuId) {
        HashMap<String, Object> resultMap = new HashMap<>();
        //1.spu
        Spu spu = spuFeign.findSpuById(spuId).getData();
        resultMap.put("spu", spu);
        //4.imageList
        String images = spu.getImages();
        if (StringUtils.isNotEmpty(images)) {
            String[] imageList = images.split(",");
            resultMap.put("imageList", imageList);
        }
        //5.specificationList
        String specs = spu.getSpec_items();
        if (StringUtils.isNotEmpty(specs)) {
            Map specMap = JSON.parseObject(specs, Map.class);
            resultMap.put("specificationList", specMap);
        }
        //1.sku
        List<Sku> skuList = skuFeign.findSkuListBySpuId(spuId);
        resultMap.put("skuList", skuList);
        //3.category
        Integer category1_id = spu.getCategory1_id();
        Integer category2_id = spu.getCategory2_id();
        Integer category3_id = spu.getCategory3_id();
        Category category1 = categoryFeign.findById(category1_id).getData();
        Category category2 = categoryFeign.findById(category2_id).getData();
        Category category3 = categoryFeign.findById(category3_id).getData();
        resultMap.put("category1", category1);
        resultMap.put("category2", category2);
        resultMap.put("category3", category3);
        return resultMap;
    }
}

3.静态页服务监听

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

    //创建静态页面的队列
    public static final String PAGE_CRATE_QUEUE = "page_create_queue";

    //定义静态页面的队列
    @Bean(PAGE_CRATE_QUEUE)
    public Queue PAGE_CREATE_QUEUE(){
        return new Queue(PAGE_CRATE_QUEUE);
    }

    //创建静态页面的队列和上架交换机关系构建
    @Bean
    public Binding GOODS_UP_EXCHANGE_PAGE_CREATE_QUEUE(
            @Qualifier(PAGE_CRATE_QUEUE)Queue queue,
            @Qualifier(GOODS_UP_EXCHANGE)Exchange exchange
    ){
        return BindingBuilder.bind(queue).to(exchange).with("").noargs();
    }

com.mall.page.listener

@Component
public class PageListener {

    @Autowired
    PageService pageService;

    @RabbitListener(queues = RabbitMQConfig.PAGE_CRATE_QUEUE)
    public void receiveMsg(String spuId){
        System.out.println("监听到商品上架了,生成静态页面:"+spuId);
        pageService.generateHtml(spuId);
    }
}

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值