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

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <modelVersion>4.0.0</modelVersion>
  6. <groupId>com.itheima</groupId>
  7. <artifactId>springboot-thymeleaf</artifactId>
  8. <version>1.0-SNAPSHOT</version>
  9. <parent>
  10. <groupId>org.springframework.boot</groupId>
  11. <artifactId>spring-boot-starter-parent</artifactId>
  12. <version>2.1.4.RELEASE</version>
  13. </parent>
  14. <dependencies>
  15. <!--web起步依赖-->
  16. <dependency>
  17. <groupId>org.springframework.boot</groupId>
  18. <artifactId>spring-boot-starter-web</artifactId>
  19. </dependency>
  20. <!--thymeleaf配置-->
  21. <dependency>
  22. <groupId>org.springframework.boot</groupId>
  23. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  24. </dependency>
  25. </dependencies>
  26. </project>

创建包com.itheima.thymeleaf 启动类

  1. @SpringBootApplication
  2. public class ThymeleafApplication {
  3. public static void main(String[] args) {
  4. SpringApplication.run(ThymeleafApplication.class,args);
  5. }
  6. }

application 设置thymeleaf的缓存设置,设置为false。默认加缓存的,用于测试。

  1. spring:
  2. thymeleaf:
  3. cache: false

创建com.itheima.controller.TestController

  1. @Controller
  2. @RequestMapping("/test")
  3. public class TestController {
  4. /***
  5. * 访问/test/hello 跳转到demo1页面
  6. * @param model
  7. * @return
  8. */
  9. @RequestMapping("/hello")
  10. public String hello(Model model){
  11. model.addAttribute("hello","hello welcome");
  12. return "demo";
  13. }
  14. }

在resources中创建templates目录,在templates目录创建 demo.html

  1. <!DOCTYPE html>
  2. <html xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4. <title>Thymeleaf的入门</title>
  5. <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
  6. </head>
  7. <body>
  8. <!--输出hello数据-->
  9. <p th:text="${hello}"></p>
  10. </body>
  11. </html>

<html xmlns:th="http://www.thymeleaf.org">:这句声明使用thymeleaf标签

<p th:text="${hello}"></p>:这句使用 th:text=”${变量名}” 表示 使用thymeleaf获取文本数据,类似于EL表达式。

2.2. 基本语法

  • th:action 定义后台控制器路径,类似<form>标签的action属性。

      1. <form th:action="@{/test/hello}" >
      2. <input th:type="text" th:name="id">
      3. <button>提交</button>
      4. </form>
  • th:each 对象遍历,功能类似jstl中的<c:forEach>标签。

      1. <table>
      2. <tr>
      3. <td>下标</td>
      4. <td>编号</td>
      5. <td>姓名</td>
      6. <td>住址</td>
      7. </tr>
      8. <tr th:each="user,userStat:${users}">
      9. <td>
      10. 下标:<span th:text="${userStat.index}"></span>,
      11. </td>
      12. <td th:text="${user.id}"></td>
      13. <td th:text="${user.name}"></td>
      14. <td th:text="${user.address}"></td>
      15. </tr>
      16. </table>
    1. /***
    2. * 访问/test/hello 跳转到demo1页面
    3. * @param model
    4. * @return
    5. */
    6. @RequestMapping("/hello")
    7. public String hello(Model model){
    8. model.addAttribute("hello","hello welcome");
    9. //集合数据
    10. List<User> users = new ArrayList<User>();
    11. users.add(new User(1,"张三","深圳"));
    12. users.add(new User(2,"李四","北京"));
    13. users.add(new User(3,"王五","武汉"));
    14. model.addAttribute("users",users);
    15. return "demo1";
    16. }
  • map输出

      1. //Map定义
      2. Map<String,Object> dataMap = new HashMap<String,Object>();
      3. dataMap.put("No","123");
      4. dataMap.put("address","深圳");
      5. model.addAttribute("dataMap",dataMap);
    1. <div th:each="map,mapStat:${dataMap}">
    2. <div th:text="${map}"></div>
    3. key:<span th:text="${mapStat.current.key}"></span><br/>
    4. value:<span th:text="${mapStat.current.value}"></span><br/>
    5. ==============================================
    6. </div>
  • 数组输出

      1. //存储一个数组
      2. String[] names = {"张三","李四","王五"};
      3. model.addAttribute("names",names);
    1. <div th:each="nm,nmStat:${names}">
    2. <span th:text="${nmStat.count}"></span><span th:text="${nm}"></span>
    3. ==============================================
    4. </div>
  • Date输出

      1. //日期
      2. model.addAttribute("now",new Date());
    1. <div>
    2. <span th:text="${#dates.format(now,'yyyy-MM-dd hh:ss:mm')}"></span>
    3. </div>
  • th:if条件

      1. //if条件
      2. model.addAttribute("age",22);
    1. <div>
    2. <span th:if="${(age>=18)}">终于长大了!</span>
    3. </div>
  • th:unless if取反

  • th:fragment 定义一个模块 并导出

      1. <!DOCTYPE html>
      2. <html xmlns:th="http://www.thymeleaf.org">
      3. <head>
      4. <meta http-equiv="Content-Type" content="text/html;charset=charset=utf-8">
      5. <title>fragment</title>
      6. </head>
      7. <body>
      8. <div id="C" th:fragment="copy" >
      9. 关于我们<br/>
      10. </div>
      11. </body>
  • th:include 导入模块 可以直接引入th:fragment,在demo1.html中引入如下代码: footer为页面文件名称

      1. <div id="A" th:include="footer::copy"></div>

3. 搜索页面渲染

08. day08 Thymeleaf - 图1

搜索的业务流程如上图,用户每次搜索的时候,先经过搜索业务工程,搜索业务工程调用搜索微服务工程。

3.1. 搜索工程搭建

在changgou-service_search工程中的pom.xml中引入如下依赖:

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  4. </dependency>

application 添加以下内容

  1. thymeleaf:
  2. cache: false

在resource下创建static包和templates包 将资源中的内容拷贝进去

3.2. 基础数据渲染

更新SearchController,定义跳转搜索结果页面方法

  1. package com.changgou.search.controller;
  2. import com.changgou.search.service.SearchService;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.stereotype.Controller;
  5. import org.springframework.ui.Model;
  6. import org.springframework.web.bind.annotation.*;
  7. import java.util.Map;
  8. import java.util.Set;
  9. @Controller
  10. @RequestMapping("/search")
  11. public class SearchController {
  12. @Autowired
  13. private SearchService searchService;
  14. @GetMapping
  15. @ResponseBody
  16. public Map search(@RequestParam Map<String, String> searchMap) {
  17. //特殊符号处理
  18. this.handleSearchMap(searchMap);
  19. Map searchResult = searchService.search(searchMap);
  20. return searchResult;
  21. }
  22. private void handleSearchMap(Map<String, String> searchMap) {
  23. Set<Map.Entry<String, String>> entries = searchMap.entrySet();
  24. for (Map.Entry<String, String> entry : entries) {
  25. if (entry.getKey().startsWith("spec_")) {
  26. searchMap.put(entry.getKey(), entry.getValue().replace("+", "%2B"));
  27. }
  28. }
  29. }
  30. @GetMapping("/list")
  31. public String list(@RequestParam Map<String, String> searchMap, Model model){
  32. //特殊符号处理
  33. this.handleSearchMap(searchMap);
  34. //获取查询结果
  35. Map resultMap = searchService.search(searchMap);
  36. model.addAttribute("result",resultMap); //写入搜索结果
  37. model.addAttribute("searchMap",searchMap); //回写搜索条件
  38. return "search";
  39. }
  40. }

搜索结果页面渲染 回显数据

添加th声明

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

第466行代码

  1. <ul class="fl sui-breadcrumb">
  2. <li>
  3. <a href="#">全部结果</a>
  4. </li>
  5. <li class="active"><span th:text="${searchMap.keywords}" ></span></li>
  6. </ul>
  7. <ul class="fl sui-tag">
  8. <li class="with-x" th:if="${(#maps.containsKey(searchMap,'brand'))}">品牌:<span th:text="${searchMap.brand}"></span><i>×</i></li>
  9. <li class="with-x" th:if="${(#maps.containsKey(searchMap,'price'))}">价格:<span th:text="${searchMap.price}"></span><i>×</i></li>
  10. <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>
  11. </ul>

3.2.1. 品牌信息显示

如果用户筛选条件携带品牌则不显示品牌列表 否则根据搜索结果中的品牌列表显示内容

第486行

  1. <div class="type-wrap logo" th:unless="${#maps.containsKey(searchMap,'brand')}">
  2. <div class="fl key brand">品牌</div>
  3. <div class="value logos">
  4. <ul class="logo-list">
  5. <li th:each="brand,brandSate:${result.brandList}"><a th:text="${brand}"></a></li>
  6. </ul>
  7. </div>

3.2.2. 商品属性及规格显示

在SearchServiceImpl添加处理规格json转换为map的方法

  1. /**
  2. * 将原有json数据转为map
  3. *
  4. * @param specList
  5. * @return
  6. */
  7. public Map<String, Set<String>> formartSpec(List<String> specList) {
  8. Map<String, Set<String>> resultMap = new HashMap<>();
  9. if (specList != null && specList.size() > 0) {
  10. for (String specJsonString : specList) {
  11. //将json转为map
  12. Map<String, String> specMap = JSON.parseObject(specJsonString, Map.class);
  13. for (String specKey : specMap.keySet()) {
  14. Set<String> specSet = resultMap.get(specKey);
  15. if (specSet == null) {
  16. specSet = new HashSet<String>();
  17. }
  18. //将规格放入set中
  19. specSet.add(specMap.get(specKey));
  20. //将set放人map中
  21. resultMap.put(specKey, specSet);
  22. }
  23. }
  24. }
  25. return resultMap;
  26. }

查询返回结果时调用此方法

  1. //封装规格分组结果
  2. StringTerms specTerms = (StringTerms) resultInfo.getAggregation(skuSpec);
  3. List<String> specList = specTerms.getBuckets().stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());
  4. resultMap.put("specList", this.formartSpec(specList));

完整Impl代码

  1. package com.changgou.search.service.impl;
  2. import com.alibaba.fastjson.JSON;
  3. import com.changgou.search.pojo.SkuInfo;
  4. import com.changgou.search.service.SearchService;
  5. import org.apache.commons.lang.StringUtils;
  6. import org.elasticsearch.action.search.SearchResponse;
  7. import org.elasticsearch.index.query.BoolQueryBuilder;
  8. import org.elasticsearch.index.query.Operator;
  9. import org.elasticsearch.index.query.QueryBuilders;
  10. import org.elasticsearch.search.SearchHit;
  11. import org.elasticsearch.search.SearchHits;
  12. import org.elasticsearch.search.aggregations.AggregationBuilders;
  13. import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
  14. import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
  15. import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
  16. import org.elasticsearch.search.sort.SortBuilders;
  17. import org.elasticsearch.search.sort.SortOrder;
  18. import org.springframework.beans.factory.annotation.Autowired;
  19. import org.springframework.data.domain.PageRequest;
  20. import org.springframework.data.domain.Pageable;
  21. import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
  22. import org.springframework.data.elasticsearch.core.SearchResultMapper;
  23. import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
  24. import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl;
  25. import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
  26. import org.springframework.stereotype.Service;
  27. import java.util.*;
  28. import java.util.stream.Collectors;
  29. @Service
  30. public class SearchServiceImpl implements SearchService {
  31. @Autowired
  32. private ElasticsearchTemplate elasticsearchTemplate;
  33. /**
  34. * 根据前端传过来的字段进行查询
  35. *
  36. * @param searchMap
  37. * @return
  38. */
  39. @Override
  40. public Map search(Map<String, String> searchMap) {
  41. Map<String, Object> resultMap = new HashMap<>();
  42. //构建查询
  43. if (searchMap != null) {
  44. //构建查询条件封装对象
  45. NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
  46. BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
  47. //按照关键字查询
  48. if (StringUtils.isNotEmpty(searchMap.get("keywords"))) {
  49. boolQuery.must(QueryBuilders.matchQuery("name", searchMap.get("keywords")).operator(Operator.AND));
  50. }
  51. //按照品牌进行过滤查询
  52. if (StringUtils.isNotEmpty(searchMap.get("brand"))) {
  53. boolQuery.filter(QueryBuilders.termQuery("brandName", searchMap.get("brand")));
  54. }
  55. //按照规格进行过滤查询
  56. for (String key : searchMap.keySet()) {
  57. if (key.startsWith("spec_")) {
  58. String value = searchMap.get(key).replace("%2B", "+");
  59. boolQuery.filter(QueryBuilders.termQuery("specMap." + key.substring(5) + ".keyword", value));
  60. }
  61. }
  62. nativeSearchQueryBuilder.withQuery(boolQuery);
  63. //按照价格进行区间过滤查询
  64. if (StringUtils.isNotEmpty(searchMap.get("price"))) {
  65. String[] prices = searchMap.get("price").split("-");
  66. if (prices.length == 2) {
  67. //是xx - xx 价格区间的条件
  68. boolQuery.filter(QueryBuilders.rangeQuery("price").lte(prices[1]));
  69. }
  70. boolQuery.filter(QueryBuilders.rangeQuery("price").gte(prices[0]));
  71. }
  72. //按照品牌进行分组(聚合)查询
  73. String skuBrand = "skuBrand";
  74. nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(skuBrand).field("brandName"));
  75. //按照规格进行聚合查询
  76. String skuSpec = "skuSpec";
  77. nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(skuSpec).field("spec.keyword"));
  78. //开启分页查询
  79. String pageNum = searchMap.get("pageNum"); //当前页
  80. String pageSize = searchMap.get("pageSize"); //每页显示多少条
  81. if (StringUtils.isEmpty(pageNum)) {
  82. pageNum = "1";
  83. }
  84. if (StringUtils.isEmpty(pageSize)) {
  85. pageSize = "30";
  86. }
  87. //设置分页
  88. nativeSearchQueryBuilder.withPageable(PageRequest.of(Integer.parseInt(pageNum) - 1, Integer.parseInt(pageSize)));
  89. //按照相关字段进行排序查询
  90. //1.当前域 2.当前的排序操作(升序ASC,降序DESC)
  91. if (StringUtils.isNotEmpty(searchMap.get("sortField")) && StringUtils.isNotEmpty(searchMap.get("sortRule"))) {
  92. if ("ASC".equals(searchMap.get("sortRule"))) {
  93. //升序操作
  94. nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(searchMap.get("sortField")).order(SortOrder.ASC));
  95. } else {
  96. //降序
  97. nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(searchMap.get("sortField")).order(SortOrder.DESC));
  98. }
  99. }
  100. //设置高亮域以及高亮的样式
  101. HighlightBuilder.Field field = new HighlightBuilder.Field("name")//高亮域
  102. //高亮前缀
  103. .preTags("<span style='color:red'>")
  104. //高亮后缀
  105. .postTags("</span>");
  106. nativeSearchQueryBuilder.withHighlightFields(field);
  107. //开启查询
  108. /**
  109. * 第一个参数: 条件的构建对象
  110. * 第二个参数: 查询操作实体类
  111. * 第三个参数: 查询结果操作对象
  112. */
  113. AggregatedPage<SkuInfo> resultInfo = elasticsearchTemplate.queryForPage(nativeSearchQueryBuilder.build(), SkuInfo.class,
  114. new SearchResultMapper() {
  115. @Override
  116. public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse, Class<T> aClass, Pageable pageable) {
  117. //查询结果操作
  118. List<T> list = new ArrayList<>();
  119. //获取查询命中的数据
  120. SearchHits hits = searchResponse.getHits();
  121. if (hits != null) {
  122. //非空
  123. for (SearchHit hit : hits) {
  124. //SearchHit转换为skuinfo
  125. SkuInfo skuInfo = JSON.parseObject(hit.getSourceAsString(), SkuInfo.class);
  126. Map<String, HighlightField> highlightFields = hit.getHighlightFields(); //获取所有高亮域
  127. if (highlightFields != null && highlightFields.size() > 0) {
  128. //替换数据
  129. skuInfo.setName(highlightFields.get("name").getFragments()[0].toString());
  130. }
  131. list.add((T) skuInfo);
  132. }
  133. }
  134. return new AggregatedPageImpl<T>(list, pageable, hits.getTotalHits(), searchResponse.getAggregations());
  135. }
  136. });
  137. //封装最终返回结果
  138. //总记录数
  139. resultMap.put("total", resultInfo.getTotalElements());
  140. //总页数
  141. resultMap.put("totalPages", resultInfo.getTotalPages());
  142. //数据集合
  143. resultMap.put("rows", resultInfo.getContent());
  144. //封装品牌的分组结果
  145. StringTerms brandTerms = (StringTerms) resultInfo.getAggregation(skuBrand);
  146. List<String> brandList = brandTerms.getBuckets().stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());
  147. resultMap.put("brandList", brandList);
  148. //封装规格分组结果
  149. StringTerms specTerms = (StringTerms) resultInfo.getAggregation(skuSpec);
  150. List<String> specList = specTerms.getBuckets().stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());
  151. resultMap.put("specList", this.formartSpec(specList));
  152. //当前页
  153. resultMap.put("pageNum", pageNum);
  154. return resultMap;
  155. }
  156. return null;
  157. }
  158. /**
  159. * 将原有json数据转为map
  160. *
  161. * @param specList
  162. * @return
  163. */
  164. public Map<String, Set<String>> formartSpec(List<String> specList) {
  165. Map<String, Set<String>> resultMap = new HashMap<>();
  166. if (specList != null && specList.size() > 0) {
  167. for (String specJsonString : specList) {
  168. //将json转为map
  169. Map<String, String> specMap = JSON.parseObject(specJsonString, Map.class);
  170. for (String specKey : specMap.keySet()) {
  171. Set<String> specSet = resultMap.get(specKey);
  172. if (specSet == null) {
  173. specSet = new HashSet<String>();
  174. }
  175. //将规格放入set中
  176. specSet.add(specMap.get(specKey));
  177. //将set放人map中
  178. resultMap.put(specKey, specSet);
  179. }
  180. }
  181. }
  182. return resultMap;
  183. }
  184. }

第625行

  1. <div class="type-wrap" th:each="spec,specStat:${result.specList}"
  2. th:unless="${#maps.containsKey(searchMap,'spec_'+spec.key)}">
  3. <div class="fl key" th:text="${spec.key}"></div>
  4. <div class="fl value">
  5. <ul class="type-list">
  6. <li th:each="op,opstat:${spec.value}">
  7. <a th:text="${op}"></a>
  8. </li>
  9. </ul>
  10. </div>
  11. <div class="fl ext"></div>
  12. </div>
  13. <div class="type-wrap" th:unless="${#maps.containsKey(searchMap,'price')}">
  14. <div class="fl key">价格</div>
  15. <div class="fl value">
  16. <ul class="type-list">
  17. <li>
  18. <a th:text="0-500元"></a>
  19. </li>
  20. <li>
  21. <a th:text="500-1000元"></a>
  22. </li>
  23. <li>
  24. <a th:text="1000-1500元"></a>
  25. </li>
  26. <li>
  27. <a th:text="1500-2000元"></a>
  28. </li>
  29. <li>
  30. <a th:text="2000-3000元"></a>
  31. </li>
  32. <li>
  33. <a th:text="3000元以上"></a>
  34. </li>
  35. </ul>
  36. </div>

3.2.3. 商品列表显示

第715行

  1. <ul class="yui3-g">
  2. <li class="yui3-u-1-5" th:each="sku,skpStat:${result.rows}">
  3. <div class="list-wrap">
  4. <div class="p-img">
  5. <a href="item.html" target="_blank"><img th:src="${sku.image}"/></a>
  6. </div>
  7. <div class="price">
  8. <strong>
  9. <em>¥</em>
  10. <i th:text="${sku.price}"></i>
  11. </strong>
  12. </div>
  13. <div class="attr">
  14. <a target="_blank" href="item.html" th:title="${sku.spec}" th:utext="${sku.name}"></a>
  15. </div>
  16. <div class="commit">
  17. <i class="command">已有<span>2000</span>人评价</i>
  18. </div>
  19. <div class="operate">
  20. <a href="success-cart.html" target="_blank" class="sui-btn btn-bordered btn-danger">加入购物车</a>
  21. <a href="javascript:void(0);" class="sui-btn btn-bordered">收藏</a>
  22. </div>
  23. </div>
  24. </li>
  25. </ul>

4. 关键字搜索

更改搜索from表单提交地址 第57行代码

  1. <form th:action="${/search/list}" class="sui-form form-inline">
  2. <!--searchAutoComplete-->
  3. <div class="input-append">
  4. <input th:type="text" th:value="${searchMap.keywords}" id="autocomplete"
  5. class="input-error input-xxlarge"/>
  6. <button class="sui-btn btn-xlarge btn-danger" th:type="submit">搜索</button>
  7. </div>
  8. </form>

将搜索输入框的内容提交给search/list 请求路径中

5. 条件搜索

08. day08 Thymeleaf - 图2

用户每次点击搜索的时候,其实在上次搜索的基础之上加上了新的搜索条件,也就是在上一次请求的URL后面追加了新的搜索条件,我们可以在后台每次拼接组装出上次搜索的URL,然后每次将URL存入到Model中,页面每次点击不同条件的时候,从Model中取出上次请求的URL,然后再加上新点击的条件参数实现跳转即可。

在SerachController的list方法添加拼接url方法

  1. //拼接url
  2. StringBuilder url = new StringBuilder("/search/list");
  3. if (searchMap != null && searchMap.size() > 0) {
  4. //map中有查询条件
  5. url.append("?");
  6. for (String paramKey : searchMap.keySet()) {
  7. if (!"sortRule".equals(paramKey) && !"sortField".equals(paramKey) && !"pageNum".equals(paramKey)){
  8. url.append(paramKey).append("=").append(searchMap.get(paramKey)).append("&");
  9. }
  10. }
  11. String urlString = url.toString();
  12. //截取最后一个&号
  13. urlString = urlString.substring(0,urlString.length()-1);
  14. model.addAttribute("url",urlString);
  15. }else {
  16. model.addAttribute("url",url);
  17. }

完整list代码

  1. @GetMapping("/list")
  2. public String list(@RequestParam Map<String, String> searchMap, Model model) {
  3. //特殊符号处理
  4. this.handleSearchMap(searchMap);
  5. //获取查询结果
  6. Map resultMap = searchService.search(searchMap);
  7. model.addAttribute("result", resultMap); //写入搜索结果
  8. model.addAttribute("searchMap", searchMap); //回写搜索条件
  9. //拼接url
  10. StringBuilder url = new StringBuilder("/search/list");
  11. if (searchMap != null && searchMap.size() > 0) {
  12. //map中有查询条件
  13. url.append("?");
  14. for (String paramKey : searchMap.keySet()) {
  15. if (!"sortRule".equals(paramKey) && !"sortField".equals(paramKey) && !"pageNum".equals(paramKey)){
  16. url.append(paramKey).append("=").append(searchMap.get(paramKey)).append("&");
  17. }
  18. }
  19. String urlString = url.toString();
  20. //截取最后一个&号
  21. urlString = urlString.substring(0,urlString.length()-1);
  22. model.addAttribute("url",urlString);
  23. }else {
  24. model.addAttribute("url",url);
  25. }
  26. return "search";
  27. }

第613行代码

  1. <div class="clearfix selector">
  2. <div class="type-wrap logo" th:unless="${#maps.containsKey(searchMap,'brand')}">
  3. <div class="fl key brand">品牌</div>
  4. <div class="value logos">
  5. <ul class="logo-list">
  6. <li th:each="brand,brandSate:${result.brandList}"><a th:text="${brand}"
  7. th:href="@{${url}(brand=${brand})}"></a>
  8. </li>
  9. </ul>
  10. </div>
  11. <div class="ext">
  12. <a href="javascript:void(0);" class="sui-btn">多选</a>
  13. <a href="javascript:void(0);">更多</a>
  14. </div>
  15. </div>
  16. <div class="type-wrap" th:each="spec,specStat:${result.specList}"
  17. th:unless="${#maps.containsKey(searchMap,'spec_'+spec.key)}">
  18. <div class="fl key" th:text="${spec.key}"></div>
  19. <div class="fl value">
  20. <ul class="type-list">
  21. <li th:each="op,opstat:${spec.value}">
  22. <a th:text="${op}" th:href="@{${url}('spec_'+${spec.key}=${op})}"></a>
  23. </li>
  24. </ul>
  25. </div>
  26. <div class="fl ext"></div>
  27. </div>
  28. <div class="type-wrap" th:unless="${#maps.containsKey(searchMap,'price')}">
  29. <div class="fl key">价格</div>
  30. <div class="fl value">
  31. <ul class="type-list">
  32. <li>
  33. <a th:text="0-500元" th:href="@{${url}(price='0-500')}"></a>
  34. </li>
  35. <li>
  36. <a th:text="500-1000元" th:href="@{${url}(price='500-1000')}"></a>
  37. </li>
  38. <li>
  39. <a th:text="1000-1500元" th:href="@{${url}(price='1000-1500')}"></a>
  40. </li>
  41. <li>
  42. <a th:text="1500-2000元" th:href="@{${url}(price='1500-2000')}"></a>
  43. </li>
  44. <li>
  45. <a th:text="2000-3000元" th:href="@{${url}(price='2000-3000')}"></a>
  46. </li>
  47. <li>
  48. <a th:text="3000元以上" th:href="@{${url}(price='3000')}"></a>
  49. </li>
  50. </ul>
  51. </div>
  52. <div class="fl ext">
  53. </div>
  54. </div>

6. 移除搜索条件

08. day08 Thymeleaf - 图3

如上图,用户点击条件搜索后,要将选中的条件显示出来,并提供移除条件的x按钮,显示条件我们可以从searchMap中获取,移除其实就是将之前的请求地址中的指定条件删除即可。

第596行

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

7. 价格排序

提供a标签拼接排序查询关键字

第711行

  1. <li>
  2. <a th:href="@{${url}(sortRule='ASC',sortField='price')}">价格↑</a>
  3. </li>
  4. <li >
  5. <a th:href="@{${url}(sortRule='DESC',sortField='price')}">价格↓</a>
  6. </li>

8. 分页搜索

将page放入changgou_common下的entity包

  1. package com.changgou.entity;
  2. import java.io.Serializable;
  3. import java.util.List;
  4. /**
  5. * 分页对象
  6. * @param <T>
  7. */
  8. public class Page <T> implements Serializable{
  9. //当前默认为第一页
  10. public static final Integer pageNum = 1;
  11. //默认每页显示条件
  12. public static final Integer pageSize = 20;
  13. //判断当前页是否为空或是小于1
  14. public static Integer cpn(Integer pageNum){
  15. if(null == pageNum || pageNum < 1){
  16. pageNum = 1;
  17. }
  18. return pageNum;
  19. }
  20. // 页数(第几页)
  21. private long currentpage;
  22. // 查询数据库里面对应的数据有多少条
  23. private long total;// 从数据库查处的总记录数
  24. // 每页显示多少分页标签
  25. private int size;
  26. // 下页
  27. private int next;
  28. private List<T> list;
  29. // 最后一页
  30. private int last;
  31. private int lpage;
  32. private int rpage;
  33. //从哪条开始查
  34. private long start;
  35. //全局偏移量
  36. public int offsize = 2;
  37. public Page() {
  38. super();
  39. }
  40. /****
  41. *
  42. * @param currentpage 当前页
  43. * @param total 总记录数
  44. * @param pagesize 每页显示多少条
  45. */
  46. public void setCurrentpage(long currentpage,long total,long pagesize) {
  47. //如果整除表示正好分N页,如果不能整除在N页的基础上+1页
  48. int totalPages = (int) (total%pagesize==0? total/pagesize : (total/pagesize)+1);
  49. //总页数
  50. this.last = totalPages;
  51. //判断当前页是否越界,如果越界,我们就查最后一页
  52. if(currentpage>totalPages){
  53. this.currentpage = totalPages;
  54. }else{
  55. this.currentpage=currentpage;
  56. }
  57. //计算起始页
  58. this.start = (this.currentpage-1)*pagesize;
  59. }
  60. /****
  61. * 初始化分页
  62. * @param total
  63. * @param currentpage
  64. * @param pagesize
  65. */
  66. public void initPage(long total,int currentpage,int pagesize){
  67. //总记录数
  68. this.total = total;
  69. //每页显示多少条
  70. this.size=pagesize;
  71. //计算当前页和数据库查询起始值以及总页数
  72. setCurrentpage(currentpage, total, pagesize);
  73. //分页计算
  74. int leftcount =this.offsize, //需要向上一页执行多少次
  75. rightcount =this.offsize;
  76. //起点页
  77. this.lpage =currentpage;
  78. //结束页
  79. this.rpage =currentpage;
  80. //2点判断
  81. this.lpage = currentpage-leftcount; //正常情况下的起点
  82. this.rpage = currentpage+rightcount; //正常情况下的终点
  83. //页差=总页数和结束页的差
  84. int topdiv = this.last-rpage; //判断是否大于最大页数
  85. /***
  86. * 起点页
  87. * 1、页差<0 起点页=起点页+页差值
  88. * 2、页差>=0 起点和终点判断
  89. */
  90. this.lpage=topdiv<0? this.lpage+topdiv:this.lpage;
  91. /***
  92. * 结束页
  93. * 1、起点页<=0 结束页=|起点页|+1
  94. * 2、起点页>0 结束页
  95. */
  96. this.rpage=this.lpage<=0? this.rpage+(this.lpage*-1)+1: this.rpage;
  97. /***
  98. * 当起点页<=0 让起点页为第一页
  99. * 否则不管
  100. */
  101. this.lpage=this.lpage<=0? 1:this.lpage;
  102. /***
  103. * 如果结束页>总页数 结束页=总页数
  104. * 否则不管
  105. */
  106. this.rpage=this.rpage>last? this.last:this.rpage;
  107. }
  108. /****
  109. *
  110. * @param total 总记录数
  111. * @param currentpage 当前页
  112. * @param pagesize 每页显示多少条
  113. */
  114. public Page(long total,int currentpage,int pagesize) {
  115. initPage(total,currentpage,pagesize);
  116. }
  117. //上一页
  118. public long getUpper() {
  119. return currentpage>1? currentpage-1: currentpage;
  120. }
  121. //总共有多少页,即末页
  122. public void setLast(int last) {
  123. this.last = (int) (total%size==0? total/size : (total/size)+1);
  124. }
  125. /****
  126. * 带有偏移量设置的分页
  127. * @param total
  128. * @param currentpage
  129. * @param pagesize
  130. * @param offsize
  131. */
  132. public Page(long total,int currentpage,int pagesize,int offsize) {
  133. this.offsize = offsize;
  134. initPage(total, currentpage, pagesize);
  135. }
  136. public long getNext() {
  137. return currentpage<last? currentpage+1: last;
  138. }
  139. public void setNext(int next) {
  140. this.next = next;
  141. }
  142. public long getCurrentpage() {
  143. return currentpage;
  144. }
  145. public long getTotal() {
  146. return total;
  147. }
  148. public void setTotal(long total) {
  149. this.total = total;
  150. }
  151. public long getSize() {
  152. return size;
  153. }
  154. public void setSize(int size) {
  155. this.size = size;
  156. }
  157. public long getLast() {
  158. return last;
  159. }
  160. public long getLpage() {
  161. return lpage;
  162. }
  163. public void setLpage(int lpage) {
  164. this.lpage = lpage;
  165. }
  166. public long getRpage() {
  167. return rpage;
  168. }
  169. public void setRpage(int rpage) {
  170. this.rpage = rpage;
  171. }
  172. public long getStart() {
  173. return start;
  174. }
  175. public void setStart(long start) {
  176. this.start = start;
  177. }
  178. public void setCurrentpage(long currentpage) {
  179. this.currentpage = currentpage;
  180. }
  181. /**
  182. * @return the list
  183. */
  184. public List<T> getList() {
  185. return list;
  186. }
  187. /**
  188. * @param list the list to set
  189. */
  190. public void setList(List<T> list) {
  191. this.list = list;
  192. }
  193. public static void main(String[] args) {
  194. //总记录数
  195. //当前页
  196. //每页显示多少条
  197. int cpage =17;
  198. Page page = new Page(1001,cpage,50,7);
  199. System.out.println("开始页:"+page.getLpage()+"__当前页:"+page.getCurrentpage()+"__结束页"+page.getRpage()+"____总页数:"+page.getLast());
  200. }
  201. }

在SearchConroller添加分页数据到页面中

  1. //封装分页数据并返回
  2. Page<SkuInfo> page =new Page<>(
  3. Long.parseLong(String.valueOf(resultMap.get("total"))), //总条数
  4. Integer.parseInt(String.valueOf(resultMap.get("pageNum"))), //当前页
  5. Page.pageSize //每页多少条数据
  6. );
  7. model.addAttribute("page",page);

完整conroller

  1. package com.changgou.search.controller;
  2. import com.changgou.entity.Page;
  3. import com.changgou.search.pojo.SkuInfo;
  4. import com.changgou.search.service.SearchService;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.stereotype.Controller;
  7. import org.springframework.ui.Model;
  8. import org.springframework.web.bind.annotation.*;
  9. import java.util.Map;
  10. import java.util.Set;
  11. @Controller
  12. @RequestMapping("/search")
  13. public class SearchController {
  14. @Autowired
  15. private SearchService searchService;
  16. @GetMapping
  17. @ResponseBody
  18. public Map search(@RequestParam Map<String, String> searchMap) {
  19. //特殊符号处理
  20. this.handleSearchMap(searchMap);
  21. Map searchResult = searchService.search(searchMap);
  22. return searchResult;
  23. }
  24. private void handleSearchMap(Map<String, String> searchMap) {
  25. Set<Map.Entry<String, String>> entries = searchMap.entrySet();
  26. for (Map.Entry<String, String> entry : entries) {
  27. if (entry.getKey().startsWith("spec_")) {
  28. searchMap.put(entry.getKey(), entry.getValue().replace("+", "%2B"));
  29. }
  30. }
  31. }
  32. @GetMapping("/list")
  33. public String list(@RequestParam Map<String, String> searchMap, Model model) {
  34. //特殊符号处理
  35. this.handleSearchMap(searchMap);
  36. //获取查询结果
  37. Map resultMap = searchService.search(searchMap);
  38. model.addAttribute("result", resultMap); //写入搜索结果
  39. model.addAttribute("searchMap", searchMap); //回写搜索条件
  40. //封装分页数据并返回
  41. Page<SkuInfo> page =new Page<>(
  42. Long.parseLong(String.valueOf(resultMap.get("total"))), //总条数
  43. Integer.parseInt(String.valueOf(resultMap.get("pageNum"))), //当前页
  44. Page.pageSize //每页多少条数据
  45. );
  46. model.addAttribute("page",page);
  47. //拼接url
  48. StringBuilder url = new StringBuilder("/search/list");
  49. if (searchMap != null && searchMap.size() > 0) {
  50. //map中有查询条件
  51. url.append("?");
  52. for (String paramKey : searchMap.keySet()) {
  53. if (!"sortRule".equals(paramKey) && !"sortField".equals(paramKey) && !"pageNum".equals(paramKey)){
  54. url.append(paramKey).append("=").append(searchMap.get(paramKey)).append("&");
  55. }
  56. }
  57. String urlString = url.toString();
  58. //截取最后一个&号
  59. urlString = urlString.substring(0,urlString.length()-1);
  60. // System.out.println(urlString);
  61. model.addAttribute("url",urlString);
  62. }else {
  63. model.addAttribute("url",url);
  64. }
  65. return "search";
  66. }
  67. }

修改th静态页面 第751行

  1. <div class="fr page">
  2. <div class="sui-pagination pagination-large">
  3. <ul>
  4. <li class="prev disabled">
  5. <a th:href="@{${url}(pageNum=${page.upper})}">«上一页</a>
  6. </li>
  7. <li th:each="i:${#numbers.sequence(page.lpage,page.rpage)}"
  8. th:class="${i}==${page.currentpage}?'active':''">
  9. <a th:href="@{${url}(pageNum=${i})}" th:text="${i}"></a>
  10. </li>
  11. <li class="dotted"><span>...</span></li>
  12. <li class="next">
  13. <a th:href="@{${url}(pageNum=${page.next})}">下一页»</a>
  14. </li>
  15. </ul>
  16. <div><span><i th:text="${page.last}"></i>页&nbsp;</span><i th:text="${page.total}"></i>个视频<span>
  17. 到第
  18. <input type="text" class="page-num">
  19. <button class="page-confirm" onclick="alert(1)">确定</button></span></div>
  20. </div>
  21. </div>
  22. </div>

9. 商品详情页

当系统审核完成商品,需要将商品详情页进行展示,那么采用静态页面生成的方式生成,并部署到高性能的web服务器中进行访问是比较合适的。所以,开发流程如下图所示:

08. day08 Thymeleaf - 图4

在changgou-service下创建一个名称为changgou_service_page的项目,作为静态化页面生成服务 添加依赖

  1. <dependencies>
  2. <dependency>
  3. <groupId>com.changgou</groupId>
  4. <artifactId>changgou_common</artifactId>
  5. <version>1.0-SNAPSHOT</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>org.springframework.boot</groupId>
  9. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  10. </dependency>
  11. <dependency>
  12. <groupId>org.springframework.boot</groupId>
  13. <artifactId>spring-boot-starter-amqp</artifactId>
  14. </dependency>
  15. <dependency>
  16. <groupId>com.changgou</groupId>
  17. <artifactId>changgou_service_goods_api</artifactId>
  18. <version>1.0-SNAPSHOT</version>
  19. </dependency>
  20. </dependencies>

application

  1. server:
  2. port: 9011
  3. spring:
  4. application:
  5. name: page
  6. rabbitmq:
  7. host: 192.168.130.128
  8. main:
  9. allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册
  10. eureka:
  11. client:
  12. service-url:
  13. defaultZone: http://127.0.0.1:6868/eureka
  14. instance:
  15. prefer-ip-address: true
  16. feign:
  17. hystrix:
  18. enabled: false
  19. client:
  20. config:
  21. default: #配置全局的feign的调用超时时间 如果 有指定的服务配置 默认的配置不会生效
  22. connectTimeout: 600000 # 指定的是 消费者 连接服务提供者的连接超时时间 是否能连接 单位是毫秒
  23. readTimeout: 600000 # 指定的是调用服务提供者的 服务 的超时时间() 单位是毫秒
  24. #hystrix 配置
  25. hystrix:
  26. command:
  27. default:
  28. execution:
  29. timeout:
  30. #如果enabled设置为false,则请求超时交给ribbon控制
  31. enabled: true
  32. isolation:
  33. strategy: SEMAPHORE
  34. # 生成静态页的位置
  35. pagepath: D:\items

在com.changgou.page 下创建启动类

  1. package com.changgou.page;
  2. import org.springframework.boot.SpringApplication;
  3. import org.springframework.boot.autoconfigure.SpringBootApplication;
  4. import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
  5. import org.springframework.cloud.openfeign.EnableFeignClients;
  6. @SpringBootApplication
  7. @EnableEurekaClient
  8. @EnableFeignClients(basePackages = {"com.changgou.goods.feign"})
  9. public class PageApplication {
  10. public static void main(String[] args) {
  11. SpringApplication.run(PageApplication.class, args);
  12. }
  13. }

9.1. 生成静态页面

9.1.1. Feign创建

需要查询SPU和SKU以及Category,所以我们需要先创建Feign,修改changgou-service-goods-api,添加CategoryFeign

  1. package com.changgou.goods.feign;
  2. import com.changgou.entity.Result;
  3. import com.changgou.goods.pojo.Category;
  4. import org.springframework.cloud.openfeign.FeignClient;
  5. import org.springframework.web.bind.annotation.GetMapping;
  6. import org.springframework.web.bind.annotation.PathVariable;
  7. @FeignClient(name = "goods")
  8. public interface CategoryFeign {
  9. @GetMapping("/category/{id}")
  10. public Result<Category> findById(@PathVariable Integer id);
  11. }

在changgou-service-goods-api,添加SkuFeign,并添加根据SpuID查询Sku集合

  1. @GetMapping("/findSpuById/{id}")
  2. public Result<Spu> findSpuById(@PathVariable String id) {
  3. Spu spu = spuService.findById(id);
  4. // Goods goods = spuService.findGoodsById(id);
  5. return new Result(true, StatusCode.OK, "查询成功", spu);
  6. }

在changgou-service-goods-api,添加SpuFeign,并添加根据SpuID查询Spu信息

  1. package com.changgou.goods.feign;
  2. import com.changgou.entity.Result;
  3. import com.changgou.goods.pojo.Spu;
  4. import org.springframework.cloud.openfeign.FeignClient;
  5. import org.springframework.web.bind.annotation.GetMapping;
  6. import org.springframework.web.bind.annotation.PathVariable;
  7. @FeignClient(name = "goods")
  8. public interface SpuFeign {
  9. @GetMapping("/spu/findSpuById/{id}")
  10. public Result<Spu> findSpuById(@PathVariable String id);
  11. }

9.1.2. 静态页面

在changgou_service_page 的com.changgou.page.service下创建

PageService

  1. package com.changgou.page.service;
  2. public interface PageService {
  3. //生成静态化页面
  4. void generateHtml(String spuId);
  5. }

impl

  1. package com.changgou.page.service.impl;
  2. import com.alibaba.fastjson.JSON;
  3. import com.changgou.entity.Result;
  4. import com.changgou.goods.feign.CategoryFeign;
  5. import com.changgou.goods.feign.SkuFeign;
  6. import com.changgou.goods.feign.SpuFeign;
  7. import com.changgou.goods.pojo.Category;
  8. import com.changgou.goods.pojo.Sku;
  9. import com.changgou.goods.pojo.Spu;
  10. import com.changgou.page.service.PageService;
  11. import org.apache.commons.lang.StringUtils;
  12. import org.springframework.beans.factory.annotation.Autowired;
  13. import org.springframework.beans.factory.annotation.Value;
  14. import org.springframework.stereotype.Service;
  15. import org.thymeleaf.TemplateEngine;
  16. import org.thymeleaf.context.Context;
  17. import java.io.File;
  18. import java.io.IOException;
  19. import java.io.PrintWriter;
  20. import java.io.Writer;
  21. import java.util.HashMap;
  22. import java.util.List;
  23. import java.util.Map;
  24. @Service
  25. public class PageServiceImpl implements PageService {
  26. @Autowired
  27. private SpuFeign spuFeign;
  28. @Autowired
  29. private CategoryFeign categoryFeign;
  30. @Autowired
  31. private SkuFeign skuFeign;
  32. @Value("${pagepath}")
  33. private String pagepath;
  34. //注入模板
  35. @Autowired
  36. private TemplateEngine templateEngine;
  37. @Override
  38. public void generateHtml(String spuId) {
  39. //获取context对象 用于存储商品的相关数据
  40. Context context = new Context();
  41. //获取静态化页面相关数据
  42. Map<String, Object> itemData = this.getItemData(spuId);
  43. context.setVariables(itemData);
  44. //获取商品详情页面的存储位置
  45. File dir = new File(pagepath);
  46. //判断当前存储位置的文件夹是否存在 如不存在则新建
  47. if (!dir.exists()) {
  48. dir.mkdirs();
  49. }
  50. //定义输出流 完成文件的生成
  51. File file = new File(dir + "/" + spuId + ".html");
  52. Writer out = null;
  53. try {
  54. out = new PrintWriter(file);
  55. //生成静态化页面
  56. /**
  57. * 1.模板名称
  58. * 2.context
  59. * 3.输出流
  60. */
  61. templateEngine.process("item", context, out);
  62. } catch (Exception e) {
  63. e.printStackTrace();
  64. } finally {
  65. //关闭流
  66. if (out != null) {
  67. try {
  68. out.close();
  69. } catch (IOException e) {
  70. e.printStackTrace();
  71. }
  72. }
  73. }
  74. }
  75. //获取静态化页面
  76. private Map<String, Object> getItemData(String spuId) {
  77. Map<String, Object> resultMap = new HashMap<>();
  78. //获取spu
  79. Spu spu = spuFeign.findSpuById(spuId).getData();
  80. resultMap.put("spu", spu);
  81. //获取图片信息
  82. if (spu != null) {
  83. if (StringUtils.isNotEmpty(spu.getImages())) {
  84. resultMap.put("imageList", spu.getImages().split(","));
  85. }
  86. }
  87. //获取商品的分类信息
  88. Category category1 = categoryFeign.findById(spu.getCategory1Id()).getData();
  89. resultMap.put("category1", category1);
  90. Category category2 = categoryFeign.findById(spu.getCategory2Id()).getData();
  91. resultMap.put("category2", category2);
  92. Category category3 = categoryFeign.findById(spu.getCategory3Id()).getData();
  93. resultMap.put("category3", category3);
  94. //获取sku的相关信息
  95. List<Sku> skuList = skuFeign.findSkuListBySpuId(spuId);
  96. resultMap.put("skuList", skuList);
  97. //获取商品规格信息
  98. resultMap.put("specificationList", JSON.parseObject(spu.getSpecItems(), Map.class));
  99. return resultMap;
  100. }
  101. }

声明page_create_queue队列,并绑定到商品上架交换机

  1. package com.changgou.page.config;
  2. import org.springframework.amqp.core.*;
  3. import org.springframework.beans.factory.annotation.Qualifier;
  4. import org.springframework.context.annotation.Bean;
  5. import org.springframework.context.annotation.Configuration;
  6. @Configuration
  7. public class RabbitMQConfig {
  8. //定义交换机名称
  9. public static final String GOODS_UP_EXCHANGE = "goods_up_exchange";
  10. public static final String GOODS_DOWN_EXCHANGE="goods_down_exchange";
  11. //定义队列名称
  12. public static final String AD_UPDATE_QUEUE = "ad_update_queue";
  13. public static final String SEARCH_ADD_QUEUE = "search_add_queue";
  14. public static final String SEARCH_DEL_QUEUE="search_del_queue";
  15. public static final String PAGE_CREATE_QUEUE="page_create_queue";
  16. //声明队列
  17. @Bean
  18. public Queue queue() {
  19. return new Queue(AD_UPDATE_QUEUE);
  20. }
  21. @Bean(SEARCH_ADD_QUEUE)
  22. public Queue SEARCH_ADD_QUEUE() {
  23. return new Queue(SEARCH_ADD_QUEUE);
  24. }
  25. @Bean(SEARCH_DEL_QUEUE)
  26. public Queue SEARCH_DEL_QUEUE(){
  27. return new Queue(SEARCH_DEL_QUEUE);
  28. }
  29. @Bean(PAGE_CREATE_QUEUE)
  30. public Queue PAGE_CREATE_QUEUE(){
  31. return new Queue(PAGE_CREATE_QUEUE);
  32. }
  33. //声明交换机
  34. @Bean(GOODS_UP_EXCHANGE)
  35. public Exchange GOODS_UP_EXCHANGE() {
  36. return ExchangeBuilder.fanoutExchange(GOODS_UP_EXCHANGE).durable(true).build();
  37. }
  38. @Bean(GOODS_DOWN_EXCHANGE)
  39. public Exchange GOODS_DOWN_EXCHANGE(){
  40. return ExchangeBuilder.fanoutExchange(GOODS_DOWN_EXCHANGE).durable(true).build();
  41. }
  42. //队列与交换机绑定
  43. @Bean
  44. public Binding GOODS_UP_EXCHANGE_BINDING(@Qualifier(SEARCH_ADD_QUEUE) Queue queue, @Qualifier(GOODS_UP_EXCHANGE) Exchange exchange) {
  45. return BindingBuilder.bind(queue).to(exchange).with("").noargs();
  46. }
  47. @Bean
  48. public Binding PAGE_CREATE_QUEUE_BINDING(@Qualifier(PAGE_CREATE_QUEUE)Queue queue,@Qualifier(GOODS_UP_EXCHANGE)Exchange exchange){
  49. return BindingBuilder.bind(queue).to(exchange).with("").noargs();
  50. }
  51. @Bean
  52. public Binding GOODS_DOWN_EXCHANGE_BINDING(@Qualifier(SEARCH_DEL_QUEUE)Queue queue,@Qualifier(GOODS_DOWN_EXCHANGE)Exchange exchange){
  53. return BindingBuilder.bind(queue).to(exchange).with("").noargs();
  54. }
  55. }

创建PageListener监听类,监听page_create_queue队列,获取消息,并生成静态化页面

  1. package com.changgou.page.listener;
  2. import com.changgou.page.config.RabbitMQConfig;
  3. import com.changgou.page.service.PageService;
  4. import org.springframework.amqp.rabbit.annotation.RabbitListener;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.stereotype.Component;
  7. @Component
  8. public class PageListener {
  9. @Autowired
  10. private PageService pageService;
  11. @RabbitListener(queues = RabbitMQConfig.PAGE_CREATE_QUEUE)
  12. public void receiveMessage(String spuId){
  13. System.out.println("获取静态化页面的id为:"+spuId);
  14. //调用业务层完成静态化页面生成
  15. pageService.generateHtml(spuId);
  16. }
  17. }

更新canal中消息队列配置类与Page服务一致

canal中的SpuListener添加监听审核状态

  1. //获取最新被审核通过的商品 status 从0变成1
  2. if ("0".equals(oldData.get("status")) && "1".equals(newData.get("status"))){
  3. //将商品的spu id 发送到mq队列中
  4. rabbitTemplate.convertAndSend(RabbitMQConfig.GOODS_UP_EXCHANGE,"",newData.get("id"));
  5. }

9.2. 模板填充

(1)面包屑数据

修改item.html,填充三个分类数据作为面包屑,代码如下:

08. day08 Thymeleaf - 图5

(2)商品图片

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

08. day08 Thymeleaf - 图6

(3)规格输出

08. day08 Thymeleaf - 图7

(4)默认SKU显示

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

08. day08 Thymeleaf - 图8

页面显示默认的Sku信息

08. day08 Thymeleaf - 图9

(5)记录选中的Sku

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

08. day08 Thymeleaf - 图10

添加规格点击事件

08. day08 Thymeleaf - 图11

(6)样式切换

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

Vue添加代码:

08. day08 Thymeleaf - 图12

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

08. day08 Thymeleaf - 图13

10. 基于nginx完成静态页访问

更改搜索页面的详情超链接

第728行

  1. <div class="p-img">
  2. <a th:href="'http://192.168.130.128:8081/'+${sku.spuId}+'.html'" target="_blank"><img th:src="${sku.image}"/></a>
  3. </div>

将静态页面上传到nginx的html