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 启动类
@SpringBootApplication
public 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 {
@Autowired
private SearchService searchService;
@GetMapping
@ResponseBody
public 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转为map
Map<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;
@Service
public class SearchServiceImpl implements SearchService {
@Autowired
private ElasticsearchTemplate elasticsearchTemplate;
/**
* 根据前端传过来的字段进行查询
*
* @param searchMap
* @return
*/
@Override
public 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() {
@Override
public <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转换为skuinfo
SkuInfo 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转为map
Map<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方法
//拼接url
StringBuilder 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); //回写搜索条件
//拼接url
StringBuilder 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'))}">品牌:<span
th: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'))}">价格:<span
th: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_')}"><span
th:text="${#strings.replace(sm.key,'spec_','')}"></span>:<span
th: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;
//判断当前页是否为空或是小于1
public 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 {
@Autowired
private SearchService searchService;
@GetMapping
@ResponseBody
public 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);
//拼接url
StringBuilder 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: 9011
spring:
application:
name: page
rabbitmq:
host: 192.168.130.128
main:
allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:6868/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: false
client:
config:
default: #配置全局的feign的调用超时时间 如果 有指定的服务配置 默认的配置不会生效
connectTimeout: 600000 # 指定的是 消费者 连接服务提供者的连接超时时间 是否能连接 单位是毫秒
readTimeout: 600000 # 指定的是调用服务提供者的 服务 的超时时间() 单位是毫秒
#hystrix 配置
hystrix:
command:
default:
execution:
timeout:
#如果enabled设置为false,则请求超时交给ribbon控制
enabled: true
isolation:
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;
@Service
public class PageServiceImpl implements PageService {
@Autowired
private SpuFeign spuFeign;
@Autowired
private CategoryFeign categoryFeign;
@Autowired
private SkuFeign skuFeign;
@Value("${pagepath}")
private String pagepath;
//注入模板
@Autowired
private TemplateEngine templateEngine;
@Override
public 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<>();
//获取spu
Spu 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;
@Configuration
public 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";
//声明队列
@Bean
public 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();
}
//队列与交换机绑定
@Bean
public 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();
}
@Bean
public 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();
}
@Bean
public 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;
@Component
public class PageListener {
@Autowired
private 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变成1
if ("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