1. day08 Thymeleaf
2. Thymeleaf
thymeleaf是一个XML/XHTML/HTML5模板引擎,可用于Web与非Web环境中的应用开发。它是一个开源的Java库,基于Apache License 2.0许可,由Daniel Fernández创建,该作者还是Java加密库Jasypt的作者。
Thymeleaf提供了一个用于整合Spring MVC的可选模块,在应用开发中,你可以使用Thymeleaf来完全代替JSP或其他模板引擎,如Velocity、FreeMarker等。Thymeleaf的主要目标在于提供一种可被浏览器正确显示的、格式良好的模板创建方式,因此也可以用作静态建模。你可以使用它创建经过验证的XML与HTML模板。相对于编写逻辑或代码,开发者只需将标签属性添加到模板中即可。接下来,这些标签属性就会在DOM(文档对象模型)上执行预先制定好的逻辑。
2.1. Springboot整合thymeleaf(入门案例)
创建一个测试thymeleaf项目 导入pom
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.itheima</groupId><artifactId>springboot-thymeleaf</artifactId><version>1.0-SNAPSHOT</version><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.4.RELEASE</version></parent><dependencies><!--web起步依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--thymeleaf配置--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency></dependencies></project>
创建包com.itheima.thymeleaf 启动类
@SpringBootApplicationpublic class ThymeleafApplication {public static void main(String[] args) {SpringApplication.run(ThymeleafApplication.class,args);}}
application 设置thymeleaf的缓存设置,设置为false。默认加缓存的,用于测试。
spring:thymeleaf:cache: false
创建com.itheima.controller.TestController
@Controller@RequestMapping("/test")public class TestController {/**** 访问/test/hello 跳转到demo1页面* @param model* @return*/@RequestMapping("/hello")public String hello(Model model){model.addAttribute("hello","hello welcome");return "demo";}}
在resources中创建templates目录,在templates目录创建 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>
<html xmlns:th="http://www.thymeleaf.org">:这句声明使用thymeleaf标签
<p th:text="${hello}"></p>:这句使用 th:text=”${变量名}” 表示 使用thymeleaf获取文本数据,类似于EL表达式。
2.2. 基本语法
th:action 定义后台控制器路径,类似
<form>标签的action属性。<form th:action="@{/test/hello}" ><input th:type="text" th:name="id"><button>提交</button></form>
th:each 对象遍历,功能类似jstl中的
<c:forEach>标签。<table><tr><td>下标</td><td>编号</td><td>姓名</td><td>住址</td></tr><tr th:each="user,userStat:${users}"><td>下标:<span th:text="${userStat.index}"></span>,</td><td th:text="${user.id}"></td><td th:text="${user.name}"></td><td th:text="${user.address}"></td></tr></table>
/**** 访问/test/hello 跳转到demo1页面* @param model* @return*/@RequestMapping("/hello")public String hello(Model model){model.addAttribute("hello","hello welcome");//集合数据List<User> users = new ArrayList<User>();users.add(new User(1,"张三","深圳"));users.add(new User(2,"李四","北京"));users.add(new User(3,"王五","武汉"));model.addAttribute("users",users);return "demo1";}
map输出
//Map定义Map<String,Object> dataMap = new HashMap<String,Object>();dataMap.put("No","123");dataMap.put("address","深圳");model.addAttribute("dataMap",dataMap);
<div th:each="map,mapStat:${dataMap}"><div th:text="${map}"></div>key:<span th:text="${mapStat.current.key}"></span><br/>value:<span th:text="${mapStat.current.value}"></span><br/>==============================================</div>
数组输出
//存储一个数组String[] names = {"张三","李四","王五"};model.addAttribute("names",names);
<div th:each="nm,nmStat:${names}"><span th:text="${nmStat.count}"></span><span th:text="${nm}"></span>==============================================</div>
Date输出
//日期model.addAttribute("now",new Date());
<div><span th:text="${#dates.format(now,'yyyy-MM-dd hh:ss:mm')}"></span></div>
th:if条件
//if条件model.addAttribute("age",22);
<div><span th:if="${(age>=18)}">终于长大了!</span></div>
th:unless if取反
th:fragment 定义一个模块 并导出
<!DOCTYPE html><html xmlns:th="http://www.thymeleaf.org"><head><meta http-equiv="Content-Type" content="text/html;charset=charset=utf-8"><title>fragment</title></head><body><div id="C" th:fragment="copy" >关于我们<br/></div></body>
th:include 导入模块 可以直接引入
th:fragment,在demo1.html中引入如下代码: footer为页面文件名称<div id="A" th:include="footer::copy"></div>
3. 搜索页面渲染

搜索的业务流程如上图,用户每次搜索的时候,先经过搜索业务工程,搜索业务工程调用搜索微服务工程。
3.1. 搜索工程搭建
在changgou-service_search工程中的pom.xml中引入如下依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency>
application 添加以下内容
thymeleaf:cache: false
在resource下创建static包和templates包 将资源中的内容拷贝进去
3.2. 基础数据渲染
更新SearchController,定义跳转搜索结果页面方法
package com.changgou.search.controller;import com.changgou.search.service.SearchService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.*;import java.util.Map;import java.util.Set;@Controller@RequestMapping("/search")public class SearchController {@Autowiredprivate SearchService searchService;@GetMapping@ResponseBodypublic Map search(@RequestParam Map<String, String> searchMap) {//特殊符号处理this.handleSearchMap(searchMap);Map searchResult = searchService.search(searchMap);return searchResult;}private void handleSearchMap(Map<String, String> searchMap) {Set<Map.Entry<String, String>> entries = searchMap.entrySet();for (Map.Entry<String, String> entry : entries) {if (entry.getKey().startsWith("spec_")) {searchMap.put(entry.getKey(), entry.getValue().replace("+", "%2B"));}}}@GetMapping("/list")public String list(@RequestParam Map<String, String> searchMap, Model model){//特殊符号处理this.handleSearchMap(searchMap);//获取查询结果Map resultMap = searchService.search(searchMap);model.addAttribute("result",resultMap); //写入搜索结果model.addAttribute("searchMap",searchMap); //回写搜索条件return "search";}}
搜索结果页面渲染 回显数据
添加th声明
<html xmlns:th="http://www.thymeleaf.org">
第466行代码
<ul class="fl sui-breadcrumb"><li><a href="#">全部结果</a></li><li class="active"><span th:text="${searchMap.keywords}" ></span></li></ul><ul class="fl sui-tag"><li class="with-x" th:if="${(#maps.containsKey(searchMap,'brand'))}">品牌:<span th:text="${searchMap.brand}"></span><i>×</i></li><li class="with-x" th:if="${(#maps.containsKey(searchMap,'price'))}">价格:<span th:text="${searchMap.price}"></span><i>×</i></li><li class="with-x" th:each="sm:${searchMap}" th:if="${#strings.startsWith(sm.key,'spec_')}"><span th:text="${#strings.replace(sm.key,'spec_','')}"></span>:<span th:text="${#strings.replace(sm.value,'%2B','+')}"></span><i>×</i></li></ul>
3.2.1. 品牌信息显示
如果用户筛选条件携带品牌则不显示品牌列表 否则根据搜索结果中的品牌列表显示内容
第486行
<div class="type-wrap logo" th:unless="${#maps.containsKey(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}"></a></li></ul></div>
3.2.2. 商品属性及规格显示
在SearchServiceImpl添加处理规格json转换为map的方法
/*** 将原有json数据转为map** @param specList* @return*/public Map<String, Set<String>> formartSpec(List<String> specList) {Map<String, Set<String>> resultMap = new HashMap<>();if (specList != null && specList.size() > 0) {for (String specJsonString : specList) {//将json转为mapMap<String, String> specMap = JSON.parseObject(specJsonString, Map.class);for (String specKey : specMap.keySet()) {Set<String> specSet = resultMap.get(specKey);if (specSet == null) {specSet = new HashSet<String>();}//将规格放入set中specSet.add(specMap.get(specKey));//将set放人map中resultMap.put(specKey, specSet);}}}return resultMap;}
查询返回结果时调用此方法
//封装规格分组结果StringTerms specTerms = (StringTerms) resultInfo.getAggregation(skuSpec);List<String> specList = specTerms.getBuckets().stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());resultMap.put("specList", this.formartSpec(specList));
完整Impl代码
package com.changgou.search.service.impl;import com.alibaba.fastjson.JSON;import com.changgou.search.pojo.SkuInfo;import com.changgou.search.service.SearchService;import org.apache.commons.lang.StringUtils;import org.elasticsearch.action.search.SearchResponse;import org.elasticsearch.index.query.BoolQueryBuilder;import org.elasticsearch.index.query.Operator;import org.elasticsearch.index.query.QueryBuilders;import org.elasticsearch.search.SearchHit;import org.elasticsearch.search.SearchHits;import org.elasticsearch.search.aggregations.AggregationBuilders;import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;import org.elasticsearch.search.sort.SortBuilders;import org.elasticsearch.search.sort.SortOrder;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.domain.PageRequest;import org.springframework.data.domain.Pageable;import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;import org.springframework.data.elasticsearch.core.SearchResultMapper;import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl;import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;import org.springframework.stereotype.Service;import java.util.*;import java.util.stream.Collectors;@Servicepublic class SearchServiceImpl implements SearchService {@Autowiredprivate ElasticsearchTemplate elasticsearchTemplate;/*** 根据前端传过来的字段进行查询** @param searchMap* @return*/@Overridepublic Map search(Map<String, String> searchMap) {Map<String, Object> resultMap = new HashMap<>();//构建查询if (searchMap != null) {//构建查询条件封装对象NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();//按照关键字查询if (StringUtils.isNotEmpty(searchMap.get("keywords"))) {boolQuery.must(QueryBuilders.matchQuery("name", searchMap.get("keywords")).operator(Operator.AND));}//按照品牌进行过滤查询if (StringUtils.isNotEmpty(searchMap.get("brand"))) {boolQuery.filter(QueryBuilders.termQuery("brandName", searchMap.get("brand")));}//按照规格进行过滤查询for (String key : searchMap.keySet()) {if (key.startsWith("spec_")) {String value = searchMap.get(key).replace("%2B", "+");boolQuery.filter(QueryBuilders.termQuery("specMap." + key.substring(5) + ".keyword", value));}}nativeSearchQueryBuilder.withQuery(boolQuery);//按照价格进行区间过滤查询if (StringUtils.isNotEmpty(searchMap.get("price"))) {String[] prices = searchMap.get("price").split("-");if (prices.length == 2) {//是xx - xx 价格区间的条件boolQuery.filter(QueryBuilders.rangeQuery("price").lte(prices[1]));}boolQuery.filter(QueryBuilders.rangeQuery("price").gte(prices[0]));}//按照品牌进行分组(聚合)查询String skuBrand = "skuBrand";nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(skuBrand).field("brandName"));//按照规格进行聚合查询String skuSpec = "skuSpec";nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(skuSpec).field("spec.keyword"));//开启分页查询String pageNum = searchMap.get("pageNum"); //当前页String pageSize = searchMap.get("pageSize"); //每页显示多少条if (StringUtils.isEmpty(pageNum)) {pageNum = "1";}if (StringUtils.isEmpty(pageSize)) {pageSize = "30";}//设置分页nativeSearchQueryBuilder.withPageable(PageRequest.of(Integer.parseInt(pageNum) - 1, Integer.parseInt(pageSize)));//按照相关字段进行排序查询//1.当前域 2.当前的排序操作(升序ASC,降序DESC)if (StringUtils.isNotEmpty(searchMap.get("sortField")) && StringUtils.isNotEmpty(searchMap.get("sortRule"))) {if ("ASC".equals(searchMap.get("sortRule"))) {//升序操作nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(searchMap.get("sortField")).order(SortOrder.ASC));} else {//降序nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(searchMap.get("sortField")).order(SortOrder.DESC));}}//设置高亮域以及高亮的样式HighlightBuilder.Field field = new HighlightBuilder.Field("name")//高亮域//高亮前缀.preTags("<span style='color:red'>")//高亮后缀.postTags("</span>");nativeSearchQueryBuilder.withHighlightFields(field);//开启查询/*** 第一个参数: 条件的构建对象* 第二个参数: 查询操作实体类* 第三个参数: 查询结果操作对象*/AggregatedPage<SkuInfo> resultInfo = elasticsearchTemplate.queryForPage(nativeSearchQueryBuilder.build(), SkuInfo.class,new SearchResultMapper() {@Overridepublic <T> AggregatedPage<T> mapResults(SearchResponse searchResponse, Class<T> aClass, Pageable pageable) {//查询结果操作List<T> list = new ArrayList<>();//获取查询命中的数据SearchHits hits = searchResponse.getHits();if (hits != null) {//非空for (SearchHit hit : hits) {//SearchHit转换为skuinfoSkuInfo skuInfo = JSON.parseObject(hit.getSourceAsString(), SkuInfo.class);Map<String, HighlightField> highlightFields = hit.getHighlightFields(); //获取所有高亮域if (highlightFields != null && highlightFields.size() > 0) {//替换数据skuInfo.setName(highlightFields.get("name").getFragments()[0].toString());}list.add((T) skuInfo);}}return new AggregatedPageImpl<T>(list, pageable, hits.getTotalHits(), searchResponse.getAggregations());}});//封装最终返回结果//总记录数resultMap.put("total", resultInfo.getTotalElements());//总页数resultMap.put("totalPages", resultInfo.getTotalPages());//数据集合resultMap.put("rows", resultInfo.getContent());//封装品牌的分组结果StringTerms brandTerms = (StringTerms) resultInfo.getAggregation(skuBrand);List<String> brandList = brandTerms.getBuckets().stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());resultMap.put("brandList", brandList);//封装规格分组结果StringTerms specTerms = (StringTerms) resultInfo.getAggregation(skuSpec);List<String> specList = specTerms.getBuckets().stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());resultMap.put("specList", this.formartSpec(specList));//当前页resultMap.put("pageNum", pageNum);return resultMap;}return null;}/*** 将原有json数据转为map** @param specList* @return*/public Map<String, Set<String>> formartSpec(List<String> specList) {Map<String, Set<String>> resultMap = new HashMap<>();if (specList != null && specList.size() > 0) {for (String specJsonString : specList) {//将json转为mapMap<String, String> specMap = JSON.parseObject(specJsonString, Map.class);for (String specKey : specMap.keySet()) {Set<String> specSet = resultMap.get(specKey);if (specSet == null) {specSet = new HashSet<String>();}//将规格放入set中specSet.add(specMap.get(specKey));//将set放人map中resultMap.put(specKey, specSet);}}}return resultMap;}}
第625行
<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}"></a></li></ul></div><div class="fl ext"></div></div><div class="type-wrap" th:unless="${#maps.containsKey(searchMap,'price')}"><div class="fl key">价格</div><div class="fl value"><ul class="type-list"><li><a th:text="0-500元"></a></li><li><a th:text="500-1000元"></a></li><li><a th:text="1000-1500元"></a></li><li><a th:text="1500-2000元"></a></li><li><a th:text="2000-3000元"></a></li><li><a th:text="3000元以上"></a></li></ul></div>
3.2.3. 商品列表显示
第715行
<ul class="yui3-g"><li class="yui3-u-1-5" th:each="sku,skpStat:${result.rows}"><div class="list-wrap"><div class="p-img"><a href="item.html" target="_blank"><img th:src="${sku.image}"/></a></div><div class="price"><strong><em>¥</em><i th:text="${sku.price}"></i></strong></div><div class="attr"><a target="_blank" href="item.html" th:title="${sku.spec}" th:utext="${sku.name}"></a></div><div class="commit"><i class="command">已有<span>2000</span>人评价</i></div><div class="operate"><a href="success-cart.html" target="_blank" class="sui-btn btn-bordered btn-danger">加入购物车</a><a href="javascript:void(0);" class="sui-btn btn-bordered">收藏</a></div></div></li></ul>
4. 关键字搜索
更改搜索from表单提交地址 第57行代码
<form th:action="${/search/list}" class="sui-form form-inline"><!--searchAutoComplete--><div class="input-append"><input th:type="text" th:value="${searchMap.keywords}" id="autocomplete"class="input-error input-xxlarge"/><button class="sui-btn btn-xlarge btn-danger" th:type="submit">搜索</button></div></form>
将搜索输入框的内容提交给search/list 请求路径中
5. 条件搜索

用户每次点击搜索的时候,其实在上次搜索的基础之上加上了新的搜索条件,也就是在上一次请求的URL后面追加了新的搜索条件,我们可以在后台每次拼接组装出上次搜索的URL,然后每次将URL存入到Model中,页面每次点击不同条件的时候,从Model中取出上次请求的URL,然后再加上新点击的条件参数实现跳转即可。
在SerachController的list方法添加拼接url方法
//拼接urlStringBuilder url = new StringBuilder("/search/list");if (searchMap != null && searchMap.size() > 0) {//map中有查询条件url.append("?");for (String paramKey : searchMap.keySet()) {if (!"sortRule".equals(paramKey) && !"sortField".equals(paramKey) && !"pageNum".equals(paramKey)){url.append(paramKey).append("=").append(searchMap.get(paramKey)).append("&");}}String urlString = url.toString();//截取最后一个&号urlString = urlString.substring(0,urlString.length()-1);model.addAttribute("url",urlString);}else {model.addAttribute("url",url);}
完整list代码
@GetMapping("/list")public String list(@RequestParam Map<String, String> searchMap, Model model) {//特殊符号处理this.handleSearchMap(searchMap);//获取查询结果Map resultMap = searchService.search(searchMap);model.addAttribute("result", resultMap); //写入搜索结果model.addAttribute("searchMap", searchMap); //回写搜索条件//拼接urlStringBuilder url = new StringBuilder("/search/list");if (searchMap != null && searchMap.size() > 0) {//map中有查询条件url.append("?");for (String paramKey : searchMap.keySet()) {if (!"sortRule".equals(paramKey) && !"sortField".equals(paramKey) && !"pageNum".equals(paramKey)){url.append(paramKey).append("=").append(searchMap.get(paramKey)).append("&");}}String urlString = url.toString();//截取最后一个&号urlString = urlString.substring(0,urlString.length()-1);model.addAttribute("url",urlString);}else {model.addAttribute("url",url);}return "search";}
第613行代码
<div class="clearfix selector"><div class="type-wrap logo" th:unless="${#maps.containsKey(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 class="ext"><a href="javascript:void(0);" class="sui-btn">多选</a><a href="javascript:void(0);">更多</a></div></div><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><div class="type-wrap" th:unless="${#maps.containsKey(searchMap,'price')}"><div class="fl key">价格</div><div class="fl value"><ul class="type-list"><li><a th:text="0-500元" th:href="@{${url}(price='0-500')}"></a></li><li><a th:text="500-1000元" th:href="@{${url}(price='500-1000')}"></a></li><li><a th:text="1000-1500元" th:href="@{${url}(price='1000-1500')}"></a></li><li><a th:text="1500-2000元" th:href="@{${url}(price='1500-2000')}"></a></li><li><a th:text="2000-3000元" th:href="@{${url}(price='2000-3000')}"></a></li><li><a th:text="3000元以上" th:href="@{${url}(price='3000')}"></a></li></ul></div><div class="fl ext"></div></div>
6. 移除搜索条件

如上图,用户点击条件搜索后,要将选中的条件显示出来,并提供移除条件的x按钮,显示条件我们可以从searchMap中获取,移除其实就是将之前的请求地址中的指定条件删除即可。
第596行
<ul class="fl sui-tag"><li class="with-x" th:if="${(#maps.containsKey(searchMap,'brand'))}">品牌:<spanth:text="${searchMap.brand}"></span><a th:href="@{${#strings.replace(url,'&brand='+searchMap.brand,'')}}">×</a></li><li class="with-x" th:if="${(#maps.containsKey(searchMap,'price'))}">价格:<spanth:text="${searchMap.price}"></span><a th:href="@{${#strings.replace(url,'&price='+searchMap.price,'')}}">×</a></li><li class="with-x" th:each="sm:${searchMap}" th:if="${#strings.startsWith(sm.key,'spec_')}"><spanth:text="${#strings.replace(sm.key,'spec_','')}"></span>:<spanth:text="${#strings.replace(sm.value,'%2B','+')}"></span><a th:href="@{${#strings.replace(url,'&'+sm.key+'='+sm.value,'')}}">×</a></li></ul>
7. 价格排序
提供a标签拼接排序查询关键字
第711行
<li><a th:href="@{${url}(sortRule='ASC',sortField='price')}">价格↑</a></li><li ><a th:href="@{${url}(sortRule='DESC',sortField='price')}">价格↓</a></li>
8. 分页搜索
将page放入changgou_common下的entity包
package com.changgou.entity;import java.io.Serializable;import java.util.List;/*** 分页对象* @param <T>*/public class Page <T> implements Serializable{//当前默认为第一页public static final Integer pageNum = 1;//默认每页显示条件public static final Integer pageSize = 20;//判断当前页是否为空或是小于1public static Integer cpn(Integer pageNum){if(null == pageNum || pageNum < 1){pageNum = 1;}return pageNum;}// 页数(第几页)private long currentpage;// 查询数据库里面对应的数据有多少条private long total;// 从数据库查处的总记录数// 每页显示多少分页标签private int size;// 下页private int next;private List<T> list;// 最后一页private int last;private int lpage;private int rpage;//从哪条开始查private long start;//全局偏移量public int offsize = 2;public Page() {super();}/****** @param currentpage 当前页* @param total 总记录数* @param pagesize 每页显示多少条*/public void setCurrentpage(long currentpage,long total,long pagesize) {//如果整除表示正好分N页,如果不能整除在N页的基础上+1页int totalPages = (int) (total%pagesize==0? total/pagesize : (total/pagesize)+1);//总页数this.last = totalPages;//判断当前页是否越界,如果越界,我们就查最后一页if(currentpage>totalPages){this.currentpage = totalPages;}else{this.currentpage=currentpage;}//计算起始页this.start = (this.currentpage-1)*pagesize;}/***** 初始化分页* @param total* @param currentpage* @param pagesize*/public void initPage(long total,int currentpage,int pagesize){//总记录数this.total = total;//每页显示多少条this.size=pagesize;//计算当前页和数据库查询起始值以及总页数setCurrentpage(currentpage, total, pagesize);//分页计算int leftcount =this.offsize, //需要向上一页执行多少次rightcount =this.offsize;//起点页this.lpage =currentpage;//结束页this.rpage =currentpage;//2点判断this.lpage = currentpage-leftcount; //正常情况下的起点this.rpage = currentpage+rightcount; //正常情况下的终点//页差=总页数和结束页的差int topdiv = this.last-rpage; //判断是否大于最大页数/**** 起点页* 1、页差<0 起点页=起点页+页差值* 2、页差>=0 起点和终点判断*/this.lpage=topdiv<0? this.lpage+topdiv:this.lpage;/**** 结束页* 1、起点页<=0 结束页=|起点页|+1* 2、起点页>0 结束页*/this.rpage=this.lpage<=0? this.rpage+(this.lpage*-1)+1: this.rpage;/**** 当起点页<=0 让起点页为第一页* 否则不管*/this.lpage=this.lpage<=0? 1:this.lpage;/**** 如果结束页>总页数 结束页=总页数* 否则不管*/this.rpage=this.rpage>last? this.last:this.rpage;}/****** @param total 总记录数* @param currentpage 当前页* @param pagesize 每页显示多少条*/public Page(long total,int currentpage,int pagesize) {initPage(total,currentpage,pagesize);}//上一页public long getUpper() {return currentpage>1? currentpage-1: currentpage;}//总共有多少页,即末页public void setLast(int last) {this.last = (int) (total%size==0? total/size : (total/size)+1);}/***** 带有偏移量设置的分页* @param total* @param currentpage* @param pagesize* @param offsize*/public Page(long total,int currentpage,int pagesize,int offsize) {this.offsize = offsize;initPage(total, currentpage, pagesize);}public long getNext() {return currentpage<last? currentpage+1: last;}public void setNext(int next) {this.next = next;}public long getCurrentpage() {return currentpage;}public long getTotal() {return total;}public void setTotal(long total) {this.total = total;}public long getSize() {return size;}public void setSize(int size) {this.size = size;}public long getLast() {return last;}public long getLpage() {return lpage;}public void setLpage(int lpage) {this.lpage = lpage;}public long getRpage() {return rpage;}public void setRpage(int rpage) {this.rpage = rpage;}public long getStart() {return start;}public void setStart(long start) {this.start = start;}public void setCurrentpage(long currentpage) {this.currentpage = currentpage;}/*** @return the list*/public List<T> getList() {return list;}/*** @param list the list to set*/public void setList(List<T> list) {this.list = list;}public static void main(String[] args) {//总记录数//当前页//每页显示多少条int cpage =17;Page page = new Page(1001,cpage,50,7);System.out.println("开始页:"+page.getLpage()+"__当前页:"+page.getCurrentpage()+"__结束页"+page.getRpage()+"____总页数:"+page.getLast());}}
在SearchConroller添加分页数据到页面中
//封装分页数据并返回Page<SkuInfo> page =new Page<>(Long.parseLong(String.valueOf(resultMap.get("total"))), //总条数Integer.parseInt(String.valueOf(resultMap.get("pageNum"))), //当前页Page.pageSize //每页多少条数据);model.addAttribute("page",page);
完整conroller
package com.changgou.search.controller;import com.changgou.entity.Page;import com.changgou.search.pojo.SkuInfo;import com.changgou.search.service.SearchService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.*;import java.util.Map;import java.util.Set;@Controller@RequestMapping("/search")public class SearchController {@Autowiredprivate SearchService searchService;@GetMapping@ResponseBodypublic Map search(@RequestParam Map<String, String> searchMap) {//特殊符号处理this.handleSearchMap(searchMap);Map searchResult = searchService.search(searchMap);return searchResult;}private void handleSearchMap(Map<String, String> searchMap) {Set<Map.Entry<String, String>> entries = searchMap.entrySet();for (Map.Entry<String, String> entry : entries) {if (entry.getKey().startsWith("spec_")) {searchMap.put(entry.getKey(), entry.getValue().replace("+", "%2B"));}}}@GetMapping("/list")public String list(@RequestParam Map<String, String> searchMap, Model model) {//特殊符号处理this.handleSearchMap(searchMap);//获取查询结果Map resultMap = searchService.search(searchMap);model.addAttribute("result", resultMap); //写入搜索结果model.addAttribute("searchMap", searchMap); //回写搜索条件//封装分页数据并返回Page<SkuInfo> page =new Page<>(Long.parseLong(String.valueOf(resultMap.get("total"))), //总条数Integer.parseInt(String.valueOf(resultMap.get("pageNum"))), //当前页Page.pageSize //每页多少条数据);model.addAttribute("page",page);//拼接urlStringBuilder url = new StringBuilder("/search/list");if (searchMap != null && searchMap.size() > 0) {//map中有查询条件url.append("?");for (String paramKey : searchMap.keySet()) {if (!"sortRule".equals(paramKey) && !"sortField".equals(paramKey) && !"pageNum".equals(paramKey)){url.append(paramKey).append("=").append(searchMap.get(paramKey)).append("&");}}String urlString = url.toString();//截取最后一个&号urlString = urlString.substring(0,urlString.length()-1);// System.out.println(urlString);model.addAttribute("url",urlString);}else {model.addAttribute("url",url);}return "search";}}
修改th静态页面 第751行
<div class="fr page"><div class="sui-pagination pagination-large"><ul><li class="prev disabled"><a th:href="@{${url}(pageNum=${page.upper})}">«上一页</a></li><li th:each="i:${#numbers.sequence(page.lpage,page.rpage)}"th:class="${i}==${page.currentpage}?'active':''"><a th:href="@{${url}(pageNum=${i})}" th:text="${i}"></a></li><li class="dotted"><span>...</span></li><li class="next"><a th:href="@{${url}(pageNum=${page.next})}">下一页»</a></li></ul><div><span>共<i th:text="${page.last}"></i>页 </span>共<i th:text="${page.total}"></i>个视频<span>到第<input type="text" class="page-num">页 <button class="page-confirm" onclick="alert(1)">确定</button></span></div></div></div></div>
9. 商品详情页
当系统审核完成商品,需要将商品详情页进行展示,那么采用静态页面生成的方式生成,并部署到高性能的web服务器中进行访问是比较合适的。所以,开发流程如下图所示:

在changgou-service下创建一个名称为changgou_service_page的项目,作为静态化页面生成服务 添加依赖
<dependencies><dependency><groupId>com.changgou</groupId><artifactId>changgou_common</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId></dependency><dependency><groupId>com.changgou</groupId><artifactId>changgou_service_goods_api</artifactId><version>1.0-SNAPSHOT</version></dependency></dependencies>
application
server:port: 9011spring:application:name: pagerabbitmq:host: 192.168.130.128main:allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册eureka:client:service-url:defaultZone: http://127.0.0.1:6868/eurekainstance:prefer-ip-address: truefeign:hystrix:enabled: falseclient:config:default: #配置全局的feign的调用超时时间 如果 有指定的服务配置 默认的配置不会生效connectTimeout: 600000 # 指定的是 消费者 连接服务提供者的连接超时时间 是否能连接 单位是毫秒readTimeout: 600000 # 指定的是调用服务提供者的 服务 的超时时间() 单位是毫秒#hystrix 配置hystrix:command:default:execution:timeout:#如果enabled设置为false,则请求超时交给ribbon控制enabled: trueisolation:strategy: SEMAPHORE# 生成静态页的位置pagepath: D:\items
在com.changgou.page 下创建启动类
package com.changgou.page;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.netflix.eureka.EnableEurekaClient;import org.springframework.cloud.openfeign.EnableFeignClients;@SpringBootApplication@EnableEurekaClient@EnableFeignClients(basePackages = {"com.changgou.goods.feign"})public class PageApplication {public static void main(String[] args) {SpringApplication.run(PageApplication.class, args);}}
9.1. 生成静态页面
9.1.1. Feign创建
需要查询SPU和SKU以及Category,所以我们需要先创建Feign,修改changgou-service-goods-api,添加CategoryFeign
package com.changgou.goods.feign;import com.changgou.entity.Result;import com.changgou.goods.pojo.Category;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;@FeignClient(name = "goods")public interface CategoryFeign {@GetMapping("/category/{id}")public Result<Category> findById(@PathVariable Integer id);}
在changgou-service-goods-api,添加SkuFeign,并添加根据SpuID查询Sku集合
@GetMapping("/findSpuById/{id}")public Result<Spu> findSpuById(@PathVariable String id) {Spu spu = spuService.findById(id);// Goods goods = spuService.findGoodsById(id);return new Result(true, StatusCode.OK, "查询成功", spu);}
在changgou-service-goods-api,添加SpuFeign,并添加根据SpuID查询Spu信息
package com.changgou.goods.feign;import com.changgou.entity.Result;import com.changgou.goods.pojo.Spu;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;@FeignClient(name = "goods")public interface SpuFeign {@GetMapping("/spu/findSpuById/{id}")public Result<Spu> findSpuById(@PathVariable String id);}
9.1.2. 静态页面
在changgou_service_page 的com.changgou.page.service下创建
PageService
package com.changgou.page.service;public interface PageService {//生成静态化页面void generateHtml(String spuId);}
impl
package com.changgou.page.service.impl;import com.alibaba.fastjson.JSON;import com.changgou.entity.Result;import com.changgou.goods.feign.CategoryFeign;import com.changgou.goods.feign.SkuFeign;import com.changgou.goods.feign.SpuFeign;import com.changgou.goods.pojo.Category;import com.changgou.goods.pojo.Sku;import com.changgou.goods.pojo.Spu;import com.changgou.page.service.PageService;import org.apache.commons.lang.StringUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Service;import org.thymeleaf.TemplateEngine;import org.thymeleaf.context.Context;import java.io.File;import java.io.IOException;import java.io.PrintWriter;import java.io.Writer;import java.util.HashMap;import java.util.List;import java.util.Map;@Servicepublic class PageServiceImpl implements PageService {@Autowiredprivate SpuFeign spuFeign;@Autowiredprivate CategoryFeign categoryFeign;@Autowiredprivate SkuFeign skuFeign;@Value("${pagepath}")private String pagepath;//注入模板@Autowiredprivate TemplateEngine templateEngine;@Overridepublic void generateHtml(String spuId) {//获取context对象 用于存储商品的相关数据Context context = new Context();//获取静态化页面相关数据Map<String, Object> itemData = this.getItemData(spuId);context.setVariables(itemData);//获取商品详情页面的存储位置File dir = new File(pagepath);//判断当前存储位置的文件夹是否存在 如不存在则新建if (!dir.exists()) {dir.mkdirs();}//定义输出流 完成文件的生成File file = new File(dir + "/" + spuId + ".html");Writer out = null;try {out = new PrintWriter(file);//生成静态化页面/*** 1.模板名称* 2.context* 3.输出流*/templateEngine.process("item", context, out);} catch (Exception e) {e.printStackTrace();} finally {//关闭流if (out != null) {try {out.close();} catch (IOException e) {e.printStackTrace();}}}}//获取静态化页面private Map<String, Object> getItemData(String spuId) {Map<String, Object> resultMap = new HashMap<>();//获取spuSpu spu = spuFeign.findSpuById(spuId).getData();resultMap.put("spu", spu);//获取图片信息if (spu != null) {if (StringUtils.isNotEmpty(spu.getImages())) {resultMap.put("imageList", spu.getImages().split(","));}}//获取商品的分类信息Category category1 = categoryFeign.findById(spu.getCategory1Id()).getData();resultMap.put("category1", category1);Category category2 = categoryFeign.findById(spu.getCategory2Id()).getData();resultMap.put("category2", category2);Category category3 = categoryFeign.findById(spu.getCategory3Id()).getData();resultMap.put("category3", category3);//获取sku的相关信息List<Sku> skuList = skuFeign.findSkuListBySpuId(spuId);resultMap.put("skuList", skuList);//获取商品规格信息resultMap.put("specificationList", JSON.parseObject(spu.getSpecItems(), Map.class));return resultMap;}}
声明page_create_queue队列,并绑定到商品上架交换机
package com.changgou.page.config;import org.springframework.amqp.core.*;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class RabbitMQConfig {//定义交换机名称public static final String GOODS_UP_EXCHANGE = "goods_up_exchange";public static final String GOODS_DOWN_EXCHANGE="goods_down_exchange";//定义队列名称public static final String AD_UPDATE_QUEUE = "ad_update_queue";public static final String SEARCH_ADD_QUEUE = "search_add_queue";public static final String SEARCH_DEL_QUEUE="search_del_queue";public static final String PAGE_CREATE_QUEUE="page_create_queue";//声明队列@Beanpublic Queue queue() {return new Queue(AD_UPDATE_QUEUE);}@Bean(SEARCH_ADD_QUEUE)public Queue SEARCH_ADD_QUEUE() {return new Queue(SEARCH_ADD_QUEUE);}@Bean(SEARCH_DEL_QUEUE)public Queue SEARCH_DEL_QUEUE(){return new Queue(SEARCH_DEL_QUEUE);}@Bean(PAGE_CREATE_QUEUE)public Queue PAGE_CREATE_QUEUE(){return new Queue(PAGE_CREATE_QUEUE);}//声明交换机@Bean(GOODS_UP_EXCHANGE)public Exchange GOODS_UP_EXCHANGE() {return ExchangeBuilder.fanoutExchange(GOODS_UP_EXCHANGE).durable(true).build();}@Bean(GOODS_DOWN_EXCHANGE)public Exchange GOODS_DOWN_EXCHANGE(){return ExchangeBuilder.fanoutExchange(GOODS_DOWN_EXCHANGE).durable(true).build();}//队列与交换机绑定@Beanpublic Binding GOODS_UP_EXCHANGE_BINDING(@Qualifier(SEARCH_ADD_QUEUE) Queue queue, @Qualifier(GOODS_UP_EXCHANGE) Exchange exchange) {return BindingBuilder.bind(queue).to(exchange).with("").noargs();}@Beanpublic Binding PAGE_CREATE_QUEUE_BINDING(@Qualifier(PAGE_CREATE_QUEUE)Queue queue,@Qualifier(GOODS_UP_EXCHANGE)Exchange exchange){return BindingBuilder.bind(queue).to(exchange).with("").noargs();}@Beanpublic Binding GOODS_DOWN_EXCHANGE_BINDING(@Qualifier(SEARCH_DEL_QUEUE)Queue queue,@Qualifier(GOODS_DOWN_EXCHANGE)Exchange exchange){return BindingBuilder.bind(queue).to(exchange).with("").noargs();}}
创建PageListener监听类,监听page_create_queue队列,获取消息,并生成静态化页面
package com.changgou.page.listener;import com.changgou.page.config.RabbitMQConfig;import com.changgou.page.service.PageService;import org.springframework.amqp.rabbit.annotation.RabbitListener;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;@Componentpublic class PageListener {@Autowiredprivate PageService pageService;@RabbitListener(queues = RabbitMQConfig.PAGE_CREATE_QUEUE)public void receiveMessage(String spuId){System.out.println("获取静态化页面的id为:"+spuId);//调用业务层完成静态化页面生成pageService.generateHtml(spuId);}}
更新canal中消息队列配置类与Page服务一致
canal中的SpuListener添加监听审核状态
//获取最新被审核通过的商品 status 从0变成1if ("0".equals(oldData.get("status")) && "1".equals(newData.get("status"))){//将商品的spu id 发送到mq队列中rabbitTemplate.convertAndSend(RabbitMQConfig.GOODS_UP_EXCHANGE,"",newData.get("id"));}
9.2. 模板填充
(1)面包屑数据
修改item.html,填充三个分类数据作为面包屑,代码如下:

(2)商品图片
修改item.html,将商品图片信息输出,在真实工作中需要做空判断,代码如下:

(3)规格输出

(4)默认SKU显示
静态页生成后,需要显示默认的Sku,我们这里默认显示第1个Sku即可,这里可以结合着Vue一起实现。可以先定义一个集合,再定义一个spec和sku,用来存储当前选中的Sku信息和Sku的规格,代码如下:

页面显示默认的Sku信息

(5)记录选中的Sku
在当前Spu的所有Sku中spec值是唯一的,我们可以根据spec来判断用户选中的是哪个Sku,我们可以在Vue中添加代码来实现,代码如下:

添加规格点击事件

(6)样式切换
点击不同规格后,实现样式选中,我们可以根据每个规格判断该规格是否在当前选中的Sku规格中,如果在,则返回true添加selected样式,否则返回false不添加selected样式。
Vue添加代码:

页面添加样式绑定,代码如下:

10. 基于nginx完成静态页访问
更改搜索页面的详情超链接
第728行
<div class="p-img"><a th:href="'http://192.168.130.128:8081/'+${sku.spuId}+'.html'" target="_blank"><img th:src="${sku.image}"/></a></div>
将静态页面上传到nginx的html
