通用型产品发布解决方案(基于分布式微服务技术栈:SpringBoot+SpringCloud+Spring CloudAlibaba+Vue+ElementUI+MyBatis-Plus+MySQL+Git+Maven+Linux+Docker+Nginx - 《04》
- GitHub:github.com/China-Rainb...
- Gitee:gitee.com/Rainbow--Se...
@[toc]
SpringBoot 使用引入 thymeleaf 标签模块的使用
引入 thymeleaf 模板(SpringBoot 推荐使用的模板,使用方便,利于 seo ,在 有 thymeleaf 标签的情况下,仍然可以正常显示,作为完整的技术体系,我们这里使用 thymeleaf 来进行一个页面的渲染。渲染页面)
使用 thymeleaf 标签模块,需要在 pom.xml 文件当中引入相关的 jar 依赖。如下:

xml
<!-- thymeleaf 模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
同时我们需要在对应的 application.yaml
文件当中配置对于 thymeleaf
模板的相关的配置信息。如下:**注意在 : yaml
当中层级关系表示的作用"缩进表示层级关系"


yaml
spring:
datasource:
username: root
password: root
url: jdbc:mysql://192.168.56.100:3306/hspliving_commodity?useUnicode=true&characterEncoding=utf-8&useSSL=false
driver-class-name: com.mysql.cj.jdbc.Driver
# driver-class-name: com.mysql.jdbc.Driver
# 配置 阿里云 oss
cloud:
# 注意: thymeleaf 是在 spring.cloud 对齐,同一层级下的
# 1. 关闭 thymeleaf 的缓存,这样当前前端页面变化时,就会看到效果
# 2. 当在生产环境时,需要将 cache 设置为 true ,表示开启 thymeleaf 的缓存机制,提高效率
thymeleaf:
cache: false
yaml
spring:
datasource:
username: root
password: root
url: jdbc:mysql://192.168.56.100:3306/hspliving_commodity?useUnicode=true&characterEncoding=utf-8&useSSL=false
driver-class-name: com.mysql.cj.jdbc.Driver
# driver-class-name: com.mysql.jdbc.Driver
# 配置 阿里云 oss
cloud:
alicloud:
oss:
endpoint: oss-cn-hangzhou.aliyuncs.com # 杭州位置
access-key: LTAI5tP4G6hDJqh7FPe1Cahh
secret-key: vl5kaBORH1QADEzKq9NInpRdD8JJeF
# 将 RainbowSealiving-commodity 模块配置注册到 nacos 上
nacos:
discovery:
server-addr: 127.0.0.1:8848 # Nacos 服务的地址
application:
name: rainbowSealiving-commodity # 该微服务的 name 信息
# 注意: thymeleaf 是在 spring.cloud 对齐,同一层级下的
# 1. 关闭 thymeleaf 的缓存,这样当前前端页面变化时,就会看到效果
# 2. 当在生产环境时,需要将 cache 设置为 true ,表示开启 thymeleaf 的缓存机制,提高效率
thymeleaf:
cache: false
# 配置 mybatis-pus
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
id-type: auto # 配置主键信息
logic-delete-value: 0 # 逻辑已经被删除值(默认为1,这里我们调整为我们自己的 0 )
logic-not-delete-value: 1 # 逻辑未被删除值(默认值为0,这里我们调整成我们自己的)
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
我们需要在创建一个 controller 是作为该项目的 index
的首页显示的作用,处理首页请求的处理。如下:

java
package com.rainbowsea.rainbowsealiving.commodity.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
/**
* @author
* @version 1.0
*/
@Controller
public class IndexController {
@GetMapping(value = {"/","index.html"})
private String indexPage(Model model) {
//默认找的是 "classpath\templates\"+"index"+".html"
// 注意:这里它默认找的是我们自己配置上的路径显示:"classpath\templates\+index+.html"
// classpath 就是我们当前模块的 resources 根路径下
return "index";
}
}

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

实现 2,3 级分类信息并显示在首页

简单分析实现思路-Model - Springboot 中的 Model 使用
首先我们在,我们的首页 indexController
处理的,增加一个返回一级分类的代码,使用 Model 模块,处理回显。

java
package com.rainbowsea.rainbowsealiving.commodity.controller;
import com.rainbowsea.rainbowsealiving.commodity.entity.CategoryEntity;
import com.rainbowsea.rainbowsealiving.commodity.service.CategoryService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
/**
* @author
* @version 1.0
*/
@Controller
public class IndexController {
@Resource
private CategoryService categoryService;
@GetMapping(value = {"/","index.html"})
private String indexPage(Model model) {
//1、查出所有的一级分类
List<CategoryEntity> categoryEntities =
categoryService.getLevel1Categorys();
model.addAttribute("categories",categoryEntities);
//默认找的是 "classpath\templates\"+"index"+".html"
// 注意:这里它默认找的是我们自己配置上的路径显示:"classpath\templates\+index+.html"
// classpath 就是我们当前模块的 resources 根路径下
return "index";
}
}

java
package com.rainbowsea.rainbowsealiving.commodity.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.rainbowsea.common.utils.PageUtils;
import com.rainbowsea.rainbowsealiving.commodity.entity.CategoryEntity;
import java.util.List;
import java.util.Map;
/**
* 商品分类表
*
* @author rainbowsea
* @email [email protected]
* @date 2025-03-04 16:38:22
*/
public interface CategoryService extends IService<CategoryEntity> {
List<CategoryEntity> getLevel1Categorys();
/**
* 返回所有分类及其子分类(层级关系-即树形)
*/
List<CategoryEntity> listTree();
PageUtils queryPage(Map<String, Object> params);
/**
* 找到 cascadedCategoryId 的[第 1 级分类 id, 第 2 级分类 id, 第 3 级分类 id]
*/
Long[] getCascadedCategoryId(Long categoryId);
}

java
package com.rainbowsea.rainbowsealiving.commodity.service.impl;
import com.rainbowsea.common.utils.PageUtils;
import com.rainbowsea.common.utils.Query;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rainbowsea.rainbowsealiving.commodity.dao.CategoryDao;
import com.rainbowsea.rainbowsealiving.commodity.entity.CategoryEntity;
import com.rainbowsea.rainbowsealiving.commodity.service.CategoryService;
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
@Override
public List<CategoryEntity> getLevel1Categorys() {
List<CategoryEntity> categoryEntities = this.baseMapper.selectList(
new QueryWrapper<CategoryEntity>().eq("parent_id", 0));
return categoryEntities;
}
}
返回 2 级和 3 级 JSON 数据的处理,进行回显处理
通过查看可以返回 1 级 /2 /3 级菜单的数据,当鼠标移动到某个 1 级分类,就会发出 ajax 请求,要相应的数据。
**难点:**前端开发人员要的 JSON 数据格式,这个完成有一定难度,分析: Map List>

VO 类的设计 这是重点,难点:这里是根据上面的 JSON 格式的显示,处理设计的一个 VO 的 Bean 类对象。这里我们使用的一个内部类的方式,存储下一层级的信息内容。

java
package com.rainbowsea.rainbowsealiving.commodity.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* @author RainbowSea
* @version 1.0
* Catalog2Vo 返回给前端的二级分类数据
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Catalog2Vo {
/**
* 一级父类的id
*/
private String catalog1Id;
/**
* 三级分类的信息-集合
*/
private List<Category3Vo> catalog3List;
/**
* 二级分类本身的信息
*/
private String id;
private String name;
/**
* 三级分类的类型-静态内部类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Category3Vo {
/**
* 父级分类-二级分类的id
*/
private String catalog2Id;
/**
* 三级分类本身的信息
*/
private String id;
private String name;
}
}
这里我们使用:流 式 计 算 Stramp API - 将 集 合 根 据 业 务 需 求 转 成 map 知 识 点 <font style="color:rgb(8,8,8);">Map<String, List<Catalog2Vo>></font>
进行一个处理显示。

java
package com.rainbowsea.rainbowsealiving.commodity.controller;
import com.rainbowsea.rainbowsealiving.commodity.entity.CategoryEntity;
import com.rainbowsea.rainbowsealiving.commodity.service.CategoryService;
import com.rainbowsea.rainbowsealiving.commodity.vo.Catalog2Vo;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
/**
* @author
* @version 1.0
*/
@Controller
public class IndexController {
@Resource
private CategoryService categoryService;
//返回 json 数据
@GetMapping(value = "/index/catalog.json")
@ResponseBody
public Map<String, List<Catalog2Vo>> getCatalogJson() {
Map<String, List<Catalog2Vo>> catalogJson =
categoryService.getCatalogJson();
return catalogJson;
}
@GetMapping(value = {"/","index.html"})
private String indexPage(Model model) {
//1、查出所有的一级分类
List<CategoryEntity> categoryEntities =
categoryService.getLevel1Categorys();
model.addAttribute("categories",categoryEntities);
//默认找的是 "classpath\templates\"+"index"+".html"
// 注意:这里它默认找的是我们自己配置上的路径显示:"classpath\templates\+index+.html"
// classpath 就是我们当前模块的 resources 根路径下
return "index";
}
}

java
package com.rainbowsea.rainbowsealiving.commodity.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.rainbowsea.common.utils.PageUtils;
import com.rainbowsea.rainbowsealiving.commodity.entity.CategoryEntity;
import com.rainbowsea.rainbowsealiving.commodity.vo.Catalog2Vo;
import java.util.List;
import java.util.Map;
/**
* 商品分类表
*
* @author rainbowsea
* @email [email protected]
* @date 2025-03-04 16:38:22
*/
public interface CategoryService extends IService<CategoryEntity> {
Map<String, List<Catalog2Vo>> getCatalogJson();
List<CategoryEntity> getLevel1Categorys();
/**
* 返回所有分类及其子分类(层级关系-即树形)
*/
List<CategoryEntity> listTree();
PageUtils queryPage(Map<String, Object> params);
/**
* 找到 cascadedCategoryId 的[第 1 级分类 id, 第 2 级分类 id, 第 3 级分类 id]
*/
Long[] getCascadedCategoryId(Long categoryId);
}

java
package com.rainbowsea.rainbowsealiving.commodity.service.impl;
import com.rainbowsea.common.utils.PageUtils;
import com.rainbowsea.common.utils.Query;
import com.rainbowsea.rainbowsealiving.commodity.vo.Catalog2Vo;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rainbowsea.rainbowsealiving.commodity.dao.CategoryDao;
import com.rainbowsea.rainbowsealiving.commodity.entity.CategoryEntity;
import com.rainbowsea.rainbowsealiving.commodity.service.CategoryService;
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList, Long parentCid) {
List<CategoryEntity> categoryEntities = selectList.stream().filter(item ->
item.getParentId().equals(parentCid)).collect(Collectors.toList());
return categoryEntities;
// return this.baseMapper.selectList(
// new QueryWrapper<CategoryEntity>().eq("parent_cid", parentCid));
}
@Override
public Map<String, List<Catalog2Vo>> getCatalogJson() {
//将数据库的多次查询变为一次
List<CategoryEntity> selectList = this.baseMapper.selectList(null);
//1、查出所有分类
//1、1)查出所有一级分类
List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);
//封装数据
Map<String, List<Catalog2Vo>> parentCid =
level1Categorys.stream().collect(Collectors.toMap(k -> k.getId().toString(), v ->
{
//1、每一个的一级分类,查到这个一级分类的二级分类
List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getId());
//2、封装上面的结果
List<Catalog2Vo> catalog2Vos = null;
if (categoryEntities != null) {
catalog2Vos = categoryEntities.stream().map(l2 -> {
Catalog2Vo catalog2Vo =
new Catalog2Vo(v.getId().toString(), null, l2.getId().toString(),
l2.getName().toString());
//1、找当前二级分类的三级分类封装成 vo
List<CategoryEntity> level3Catelog = getParent_cid(selectList,
l2.getId());
if (level3Catelog != null) {
List<Catalog2Vo.Category3Vo> category3Vos =
level3Catelog.stream().map(l3 -> {
//2、封装成指定格式
Catalog2Vo.Category3Vo category3Vo =
new Catalog2Vo.Category3Vo(l2.getId().toString(),
l3.getId().toString(), l3.getName());
return category3Vo;
}).collect(Collectors.toList());
catalog2Vo.setCatalog3List(category3Vos);
}
return catalog2Vo;
}).collect(Collectors.toList());
}
return catalog2Vos;
}));
return parentCid;
}
@Override
public List<CategoryEntity> getLevel1Categorys() {
List<CategoryEntity> categoryEntities = this.baseMapper.selectList(
new QueryWrapper<CategoryEntity>().eq("parent_id", 0));
return categoryEntities;
}
/**
* 核心方法: 返回所有分类及其子分类(带有层级关系-即树形)
* 这里我们会使用 java8的,流式计算(stream api) + 递归操作(有一定难度)
*
* @return
*/
@Override
public List<CategoryEntity> listTree() {
// 老韩思路分析-步骤:
// 1. 查出所有的分类数据
List<CategoryEntity> entities = baseMapper.selectList(null);
// 2.组装成层级树形结构使用到 Java8 的 stream api + 递归操作
// 思路:
// 1.过滤,返回1级分类
// 2.2 进行 map 映射操作,给每个分类设置对应的子分类(这个过程会使用到递归)
// 2.3 进行排序 sorted 操作
// 2.4 将处理好的数据收集/转换到集合
// 3.返回 带有层级关系数据-即树形
// 需求:从 List 中过滤出 person.id % 2 != 0 的 person对象
// list.stream() : 把 List 转成流对象,目的是为了使用流的方法,
// 这样就可以处理一些比较负载的业务
List<CategoryEntity> categoryTree =
entities.stream().filter(categoryEntity -> {
// 2.1 过滤filter,返回 1级分类
return categoryEntity.getParentId() == 0; // 0 就是一级分类
}).map(category -> {
// 2.2 进行map映射操作,给每个分类设置对应的子分类(这个过程会使用到递归)
category.setChildrenCategories(getChildrenCategories(category, entities));
return category;
}).sorted((category1, category2) -> {
// 2.3 进行排序sorted 操作,按照 sort 的升序排列
return (category1.getSort() == null ? 0 : category1.getSort()) -
(category2.getSort() == null ? 0 : category2.getSort());
}).collect(Collectors.toList()); // // 2.4 将处理好的数据收集 collect/转换到集合中
// 3. 返回带有层级关系的-即树形
return categoryTree;
}
}

javascript
$.getJSON("index/catalog.json", function (data) {
var ctgall = data;
$(".header_main_left_a").each(function () {
var ctgnums = $(this).attr("ctg-data");
if (ctgnums) {
var panel = $("<div class='header_main_left_main'></div>");
var panelol = $("<ol class='header_ol'></ol>");
var ctgnumArray = ctgnums.split(",");
$.each(ctgnumArray, function (i, ctg1Id) {
var ctg2list = ctgall[ctg1Id];
$.each(ctg2list, function (i, ctg2) {
var cata2link = $("<a href='#' style= 'color: #111;' class='aaa'>" + ctg2.name + " ></a>");
console.log(cata2link.html());
var li = $("<li></li>");
var ctg3List = ctg2["catalog3List"];
var len = 0;
$.each(ctg3List, function (i, ctg3) {
var cata3link = $("<a href=\"http://localhost:9090/list.html?catalog3Id=" + ctg3.id + "\" style=\"color: #999;\">" + ctg3.name + "</a>");
li.append(cata3link);
len = len + 1 + ctg3.name.length;
});
if (len >= 46 && len < 92) {
li.attr("style", "height: 60px;");
} else if (len >= 92) {
li.attr("style", "height: 90px;");
}
panelol.append(cata2link).append(li);
});
});
panel.append(panelol);
$(this).after(panel);
$(this).parent().addClass("header_li2");
console.log($(".header_main_left").html());
}
});
});
javascript
$(function () {
$.getJSON("index/catalog.json", function (data) {
var ctgall = data;
$(".header_main_left_a").each(function () {
var ctgnums = $(this).attr("ctg-data");
if (ctgnums) {
var panel = $("<div class='header_main_left_main'></div>");
var panelol = $("<ol class='header_ol'></ol>");
var ctgnumArray = ctgnums.split(",");
$.each(ctgnumArray, function (i, ctg1Id) {
var ctg2list = ctgall[ctg1Id];
$.each(ctg2list, function (i, ctg2) {
var cata2link = $("<a href='#' style= 'color: #111;' class='aaa'>" + ctg2.name + " ></a>");
console.log(cata2link.html());
var li = $("<li></li>");
var ctg3List = ctg2["catalog3List"];
var len = 0;
$.each(ctg3List, function (i, ctg3) {
var cata3link = $("<a href=\"http://localhost:9090/list.html?catalog3Id=" + ctg3.id + "\" style=\"color: #999;\">" + ctg3.name + "</a>");
li.append(cata3link);
len = len + 1 + ctg3.name.length;
});
if (len >= 46 && len < 92) {
li.attr("style", "height: 60px;");
} else if (len >= 92) {
li.attr("style", "height: 90px;");
}
panelol.append(cata2link).append(li);
});
});
panel.append(panelol);
$(this).after(panel);
$(this).parent().addClass("header_li2");
console.log($(".header_main_left").html());
}
});
});
});
运行测试:

注意:locak 生成的无参/有参数的构造器的方法,的逻辑顺序的,是属性字段的前后顺序决定的
分页导航处理
完成商品点 SPU 信息的检索(分页+ 条件)
- 检索的条件这里设置的是为:分类,品牌,状态和关键字
- 特殊的:说明输入的关键字,我们定义的是一个业务的需求,如果视为 ID 就是等于 ,如果视为名称就是模糊查询。
带有条件的分页查询:

java
import java.util.Arrays;
import java.util.Map;
//import org.apache.shiro.authz.annotation.RequiresPermissions;
import com.rainbowsea.rainbowsealiving.commodity.service.SkuInfoService;
import com.rainbowsea.rainbowsealiving.commodity.vo.SpuSaveVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.rainbowsea.rainbowsealiving.commodity.entity.SpuInfoEntity;
import com.rainbowsea.rainbowsealiving.commodity.service.SpuInfoService;
import com.rainbowsea.common.utils.PageUtils;
import com.rainbowsea.common.utils.R;
import javax.annotation.Resource;
/**
* 商品 spu 信息
*
* @author rainbowsea
* @email [email protected]
* @date 2025-03-24 20:31:43
*/
@RestController
@RequestMapping("commodity/spuinfo")
public class SpuInfoController {
@Autowired
private SpuInfoService spuInfoService;
@Resource
private SkuInfoService skuInfoService;
//商品上架
@PostMapping(value = "/{spuId}/up")
public R spuUp(@PathVariable("spuId") Long spuId) {
spuInfoService.up(spuId);
return R.ok();
}
//商品下架
@PostMapping(value = "/{spuId}/down")
public R spuDown(@PathVariable("spuId") Long spuId) {
spuInfoService.down(spuId);
return R.ok();
}
/**
* 列表
*/
@RequestMapping("/list")
//@RequiresPermissions("commodity:spuinfo:list")
public R list(@RequestParam Map<String, Object> params) {
//PageUtils page = spuInfoService.queryPage(params);//注销
//换成 带条件查询
// PageUtils page = spuInfoService.queryPageByCondition(params);
/**
* 带条件分页查询
*/
PageUtils page = skuInfoService.queryPageByCondition(params);
return R.ok().put("page", page);
}
}

java
package com.rainbowsea.rainbowsealiving.commodity.service.impl;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.Map;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rainbowsea.common.utils.PageUtils;
import com.rainbowsea.common.utils.Query;
import com.rainbowsea.rainbowsealiving.commodity.dao.SkuInfoDao;
import com.rainbowsea.rainbowsealiving.commodity.entity.SkuInfoEntity;
import com.rainbowsea.rainbowsealiving.commodity.service.SkuInfoService;
@Service("skuInfoService")
public class SkuInfoServiceImpl extends ServiceImpl<SkuInfoDao, SkuInfoEntity> implements SkuInfoService {
@Override
public PageUtils queryPageByCondition(Map<String, Object> params) {
QueryWrapper<SkuInfoEntity> queryWrapper = new QueryWrapper<>();
//带上查询条件
String key = (String) params.get("key");
if (!StringUtils.isEmpty(key)) {
queryWrapper.and((wrapper) -> {
wrapper.eq("sku_id", key).or().like("sku_name", key);
});
}
//带上分类
String catelogId = (String) params.get("catelogId");
if (!StringUtils.isEmpty(catelogId) && !"0".equalsIgnoreCase(catelogId)) {
queryWrapper.eq("catalog_id", catelogId);
}
//带上品牌
String brandId = (String) params.get("brandId");
if (!StringUtils.isEmpty(brandId) && !"0".equalsIgnoreCase(brandId)) {
queryWrapper.eq("brand_id", brandId);
}
//价格范围
String min = (String) params.get("min");
if (!StringUtils.isEmpty(min)) {
queryWrapper.ge("price", min);
}
String max = (String) params.get("max");
//校验传递的价格范围合理性, 如果 max 有值,并且大于 0,
//才有必要封装到查询条件
if (!StringUtils.isEmpty(max)) {
try {
BigDecimal bigDecimal = new BigDecimal(max);
if (bigDecimal.compareTo(new BigDecimal("0")) == 1) {
queryWrapper.le("price", max);
}
} catch (Exception e) {
}
}
IPage<SkuInfoEntity> page = this.page(
new Query<SkuInfoEntity>().getPage(params), queryWrapper
);
return new PageUtils(page);
}
@Override
public void saveSkuInfo(SkuInfoEntity skuInfoEntity) {
this.baseMapper.insert(skuInfoEntity);
}
@Override
public PageUtils queryPage(Map<String, Object> params) {
IPage<SkuInfoEntity> page = this.page(
new Query<SkuInfoEntity>().getPage(params),
new QueryWrapper<SkuInfoEntity>()
);
return new PageUtils(page);
}
}
实现搜索框的检索
引入家居上线搜索首页,并配置能正常显示

家居检索页面,可以根据查询条件分页返回数据,并在家居网前台页面显示
完成功能说明: 带条件分页查询, 可以对 关键字/分类 进行分页检索

业务需求:前端页面/html 模板,需要的数据形式为,后端返回检索结果的业务处理,都需要以此为基础
重点: JSON 格式 vue-springboot
如下分析如下 JSON 格式的内容,如下的 JSON 格式就是我们
json
(commodity=[SkuInfoEntity(skuId=25, spuId=14, skuName= 海 信 21-21 100*100 黑 色 ,
skuDesc=null,
catalogId=301,brandId=1,skuDefaultImg=https://hspliving-10002.oss-cn-beijing.aliyuncs.com/
15-hsp.jpg, skuTitle= 海 信 21-21 100*100 黑 色 , skuSubtitle=, price=0.0000, saleCount=0),
SkuInfoEntity(skuId=26, spuId=14, skuName= 海 信 21-21 100*100 红 色 , skuDesc=null,
catalogId=301, brandId=1,
skuDefaultImg=https://hspliving-10002.oss-cn-beijing.aliyuncs.com/15-hsp.jpg, skuTitle=海信
21-21 100*100 红 色 , skuSubtitle=, price=0.0000, saleCount=0)], pageNum=1, total=2,
totalPages=1, pageNavs=[1])
创建一个 Bean 实体类对象,用于存储我上面这个前端所需要的 JSON 格式的对象格式表示。
如下我们创建一个:这个 Bean 实体类是根据具体前端所需要的具体内容所设置的。

java
package com.rainbowsea.rainbowsealiving.commodity.vo;
import com.rainbowsea.rainbowsealiving.commodity.entity.SkuInfoEntity;
import lombok.Data;
import java.util.List;
@Data
public class SearchResult {
/**
* 查询到的所有家居商品信息
*/
private List<SkuInfoEntity> commodity;
/**
* 当前页码
*/
private Integer pageNum;
/**
* 总记录数
*/
private Long total;
/**
* 总页码
*/
private Integer totalPages;
private List<Integer> pageNavs;
}
SearchController.java , 增加分页检索代码

java
import com.rainbowsea.common.utils.PageUtils;
import com.rainbowsea.rainbowsealiving.commodity.entity.SkuInfoEntity;
import com.rainbowsea.rainbowsealiving.commodity.service.SkuInfoService;
import com.rainbowsea.rainbowsealiving.commodity.vo.SearchResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Controller
public class SearchController {
@Resource
private SkuInfoService skuInfoService;
// 注意:这里我们使用了一个方法的重载,这个方法多了一个 Model参数
@RequestMapping("/list.html")
public String searchList
(@RequestParam Map<String, Object> params,Model model, HttpServletRequest request) {
/**
* 带条件分页查询
*/
PageUtils page = skuInfoService.querySearchPageByCondition(params);
//将 page 转成前端需要的数据格式[注意,不同前端页面要的结果可能不一样]
SearchResult searchResult = new SearchResult();
searchResult.setTotal((long) page.getTotalCount());
int totalPage = page.getTotalPage();
searchResult.setTotalPages(page.getTotalPage());
searchResult.setPageNum(page.getCurrPage());
searchResult.setCommodity((List<SkuInfoEntity>) page.getList());
List<Integer> pageNavs = new ArrayList<>();
for (int i = 1; i <= totalPage; i++) {
pageNavs.add(i);
}
searchResult.setPageNavs(pageNavs);
model.addAttribute("result", searchResult);
System.out.println("result= " + searchResult);
return "list";
}
}

java
package com.rainbowsea.rainbowsealiving.commodity.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.rainbowsea.common.utils.PageUtils;
import com.rainbowsea.rainbowsealiving.commodity.entity.SkuInfoEntity;
import java.util.Map;
/**
* sku 信息
*
* @author rainbowsea
* @email [email protected]
* @date 2025-03-24 22:01:45
*/
public interface SkuInfoService extends IService<SkuInfoEntity> {
//返回家居网前台,购买用户检索结果
PageUtils querySearchPageByCondition(Map<String, Object> params);
PageUtils queryPageByCondition(Map<String, Object> params);
PageUtils queryPage(Map<String, Object> params);
void saveSkuInfo(SkuInfoEntity skuInfoEntity);
}

java
package com.rainbowsea.rainbowsealiving.commodity.service.impl;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.Map;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rainbowsea.common.utils.PageUtils;
import com.rainbowsea.common.utils.Query;
import com.rainbowsea.rainbowsealiving.commodity.dao.SkuInfoDao;
import com.rainbowsea.rainbowsealiving.commodity.entity.SkuInfoEntity;
import com.rainbowsea.rainbowsealiving.commodity.service.SkuInfoService;
@Service("skuInfoService")
public class SkuInfoServiceImpl extends ServiceImpl<SkuInfoDao, SkuInfoEntity> implements SkuInfoService {
@Override
public PageUtils querySearchPageByCondition
(Map<String, Object> params) {
QueryWrapper<SkuInfoEntity> queryWrapper = new QueryWrapper<>();
//带上查询条件
String key = (String) params.get("keyword");
if(!StringUtils.isEmpty(key)){
queryWrapper.and((wrapper)->{
wrapper.eq("sku_id",key).or().like("sku_name",key);
});
}
//带上分类[第三级分类]
String catelogId = (String) params.get("catalog3Id");
if(!StringUtils.isEmpty(catelogId)&&!"0".equalsIgnoreCase(catelogId)){
queryWrapper.eq("catalog_id",catelogId);
}
//带上品牌
String brandId = (String) params.get("brandId");
if(!StringUtils.isEmpty(brandId)&&!"0".equalsIgnoreCase(brandId)){
queryWrapper.eq("brand_id",brandId);
}
IPage<SkuInfoEntity> page = this.page(
new Query<SkuInfoEntity>().getPage(params),
queryWrapper
);
return new PageUtils(page);
}
@Override
public PageUtils queryPageByCondition(Map<String, Object> params) {
QueryWrapper<SkuInfoEntity> queryWrapper = new QueryWrapper<>();
//带上查询条件
String key = (String) params.get("key");
if (!StringUtils.isEmpty(key)) {
queryWrapper.and((wrapper) -> {
wrapper.eq("sku_id", key).or().like("sku_name", key);
});
}
//带上分类
String catelogId = (String) params.get("catelogId");
if (!StringUtils.isEmpty(catelogId) && !"0".equalsIgnoreCase(catelogId)) {
queryWrapper.eq("catalog_id", catelogId);
}
//带上品牌
String brandId = (String) params.get("brandId");
if (!StringUtils.isEmpty(brandId) && !"0".equalsIgnoreCase(brandId)) {
queryWrapper.eq("brand_id", brandId);
}
//价格范围
String min = (String) params.get("min");
if (!StringUtils.isEmpty(min)) {
queryWrapper.ge("price", min);
}
String max = (String) params.get("max");
//校验传递的价格范围合理性, 如果 max 有值,并且大于 0,
//才有必要封装到查询条件
if (!StringUtils.isEmpty(max)) {
try {
BigDecimal bigDecimal = new BigDecimal(max);
if (bigDecimal.compareTo(new BigDecimal("0")) == 1) {
queryWrapper.le("price", max);
}
} catch (Exception e) {
}
}
IPage<SkuInfoEntity> page = this.page(
new Query<SkuInfoEntity>().getPage(params), queryWrapper
);
return new PageUtils(page);
}
@Override
public void saveSkuInfo(SkuInfoEntity skuInfoEntity) {
this.baseMapper.insert(skuInfoEntity);
}
@Override
public PageUtils queryPage(Map<String, Object> params) {
IPage<SkuInfoEntity> page = this.page(
new Query<SkuInfoEntity>().getPage(params),
new QueryWrapper<SkuInfoEntity>()
);
return new PageUtils(page);
}
}
取出返回的 result ,并通过 thymeleaf 渲染页面
html
<!--排序内容-->
<div class="rig_tab">
<div th:each="commodity : ${result.getCommodity()}">
<div class="ico">
<i class="iconfont icon-weiguanzhu"></i>
<a href="#">关注</a>
</div>
<p class="da">
<a href="#" title="购买 AppleCare+,获得原厂 2 年整机保修(含电池),和多达 2
次意外损坏的保修服务。购买勾选:保障服务、原厂保 2 年。">
<img th:src="${commodity.skuDefaultImg}" class="dim">
</a>
</p>
<ul class="tab_im">
<li><a href="#" title="黑色">
<img th:src="${commodity.skuDefaultImg}"></a></li>
</ul>
<p class="tab_R">
<span th:text="'¥' + ${commodity.price}">¥5199.00</span>
</p>
<p class="tab_JE">
<a href="#" th:utext="${commodity.skuTitle}">
北欧风格沙发 10000#号
</a>
</p>
<p class="tab_PI">已有<span>11 万+</span>热门评价
<a href="#">二手有售</a>
</p>
<p class="tab_CP"><a href="#" title="家居网 Apple 产品专营店">家居网 Apple 产
品...</a>
<a href='#' title="联系供应商进行咨询">
<img src="search/img/xcxc.png">
</a>
</p>
<div class="tab_FO">
<div class="FO_one">
<p>自营
<span>家居网自营,品质保证</span>
</p>
<p>满赠
<span>该商品参加满赠活动</span>
</p>
</div>
</div>
</div>
</div>
<!--分页-->
<div class="filter_page">
<div class="page_wrap">
<span class="page_span1">
<a href="#" th:attr="pn=${result.pageNum - 1}" th:if="${result.pageNum>1}">< 上一页</a>
<a class="page_a"
th:attr="pn=${navs},style=${navs == result.pageNum?'border:0;color:#ee2222;background: #fff':''}"
th:each="navs : ${result.pageNavs}">[[${navs}]]</a>
<a href="#" th:attr="pn=${result.pageNum + 1}"
th:if="${result.pageNum<result.totalPages}">下一页 ></a>
</span><span class="page_span2">
<em>共<b>[[${result.totalPages}]]</b>页 到第</em>
<input type="number" value="1">
<em>页</em>
<a href="#">确定</a></span>
</div>
</div>

- limit 参数,表示每页显示几条记录
- keyword 参数,表示按照 id 或者名字作为关键字检索
- catalog3ld 参数,表示按照分类。
说明:图片,没有显示,是因为后台在发布产品的问题,只要后台发布家居产品能正常显示图片,前端页面就会正常显示。
支持在搜索框输入关键字, 进行检索
http://localhost:9090/list.html?keyword=%E6%B5%B7%E4%BF%A1&catalog3Id=301

list.html , 增加当用户输入条件,点击搜索按钮的处理代码.

html
<div class="header_form">
<input id="keyword_input" type="text" placeholder="家居"/>
<a href="javascript:searchByKeyword()">搜索</a>
</div>
加入 Nginx - 完成反向代理,负载均衡,动静分离
首先使用 XShell 登录 VB 虚拟机,这里我们需配置 sshd
- sudo vi /etc/ssh/sshd_config
shell
sudo vi /etc/ssh/sshd_config
- 将 PasswordAuthentication 的 no 改成 yes

- 重启服务 service sshd restart
提醒: 修改文件权限不够,请使用 sudo
ip a 查看 IP 地址
shell
ip a

用户名 root : 密码是: vagrant
测试是否可以进入到 mysql
shell
sudo docker restart mysql
sudo docker exec -it mysql /bin/bash

在 Linux 安装&配置 Nginx -能正确访问到 Nginx
首先需要确保,我们的虚拟机 Linux 可以访问到外网:
shell
[root@localhost ~]# ping www.baidu.com

这里我们将 Nginx 安装在 mydata 目录下。
shell
[root@localhost ~]# cd /mydata

在 mydata 目录下创建一个 nginx 目录用于存放安装的 Nginx
shell
[root@localhost mydata]# mkdir nginx

安装,拉取 Nginx1.10 的镜像
shell
[root@localhost mydata]# docker run -p 80:80 --name nginx -d nginx:1.10

- 将容器内的配置文件拷贝到当前目录

shell
[root@localhost mydata]# cd nginx/
shell
[root@localhost mydata]# docker container cp nginx:/etc/nginx .
Successfully copied 26.6kB to /mydata/.


- 终止原容器, 并删除原容器, 保留配置文件即可
shell
[root@localhost nginx]# docker stop nginx
nginx
[root@localhost nginx]# docker rm nginx
nginx
[root@localhost nginx]#

- 修改文件名, 并把 conf 移动到 /mydata/nginx 下
shell
[root@localhost mydata]# mv nginx/ conf

shell
[root@localhost mydata]# mv conf/ ./nginx/



- 创建新的 nginx
特别注意:必须必须必须,对于配置的 conf 配置文件的路径位置存在,才会创建 Nginc 成功,如果对于路径没有 conf 文件/目录,是会创建失败的。
shell
[root@localhost conf]# docker run --name nginx -p 80:80 \
> -v/mydata/nginx/html:/usr/share/nginx/html \
> -v /mydata/nginx/logs:/var/log/nginx \
> -v/mydata/nginx/conf:/etc/nginx \
> --privileged=true -itd nginx:1.10

shell
docker run --name nginx -p 80:80 \
-v/mydata/nginx/html:/usr/share/nginx/html \
-v /mydata/nginx/logs:/var/log/nginx \
-v/mydata/nginx/conf:/etc/nginx \
--privileged=true -itd nginx:1.10
如果 docker ps 发现 nginx 并没有启动的话,可以执行
shell
[root@localhost nginx]# docker run -d nginx:1.10

- 在 nginx/html 目录下创建 index.html


html
<h1>RainbowSealiving</h1>
在 windows 访问 nginx 的 index.html , 默认端口是 80 , 如果访问不到, 检查网络是否畅
通, 防火墙是否打开了该端口, 这些我们在讲解 Linux 时候, 是讲过的



访问成功
Nginx + Windows 搭建域名访问环境
也就是通过域名来访问网站, 比如 rainbowsealiving.com
-
分析我们当前项目的架构情况
-
通过 Nginx 可以实现 反向代理、负载均衡和动静分离

搭建域名环境
- 注意需要让虚拟机 Linux 可以 ping 主机。
- 同时也要让主机 可以 Ping Linux
配置域名位置: C:\Windows\System32\drivers\etc\hosts
配置 hspliving, 根据实际情况配置

shell
192.168.56.100 www.RainbowSealiving.com
完成测试 , 浏览器输入 www.rainbowsealiving.com/

注意一个小细节 , 访问的 url 不要写成 https://www.rainbowsealiving.com/, 否则访问不到
配置 Nginx 完成反向代理
让 Nginx 完成反向代理, 所有来自 hspliving.com 的请求都转到 家居商品服务
HsplivingCommodityApplication :9090/ - 暂时不使用网关,后面再整合, 一步一步来

- 首先我们将 default.conf 的备份一份,复制命名为 rainbowsealiving.conf
shell
cp default.conf rainbowsealiving.conf
shell
[root@localhost conf.d]# cp default.conf rainbowsealiving.conf


shell
[root@localhost conf.d]# vi rainbowsealiving.conf

更新了 Nginx 配置,重启 Nginx 读取配置
shell
[root@localhost conf.d]# docker restart nginx

另外一定要保证 你的虚拟机 可以 访问到 windows 部署的 各个服务,即网络是畅通
的比如这里 虚拟机 ip 是 192.168.56.100 windows 是 192.168.56.1 就必须可以 ping 通测试的时候,可以暂时关闭 windows 的防火墙

配置负载均衡到网关
- 配置负载均衡到网关,即加入网关实现负载均衡
- 请求->nginx->网关->真正的服务

配置实现
- vi /mydata/nginx/conf/nginx.conf
shell
[root@localhost conf.d]# vi /mydata/nginx/conf/nginx.conf

properties
upstream rainbowsealiving {
server 192.168.153.1:5050;
}
- vi /mydata/nginx/conf/conf.d/hspliving.conf
shell
[root@localhost conf.d]# vi /mydata/nginx/conf/conf.d/rainbowsealiving.conf

shell
location / {
proxy_pass http://rainbowsealiving;
}
- sudo docker restart nginx //重启 nginx
shell
[root@localhost conf.d]# sudo docker restart nginx



yaml
# for nginx 增加一组路由
- id: hspliving_host_route
uri: lb://rainbowSealiving-commodity
predicates:
- Host=**.rainbowsealiving.com
yaml
server:
port: 5050 #gateway监听端口
spring:
cloud:
#配置网关
#http://localhost:5050/api/commodity/brand/list
#http://www.hspliving.com/api/commodity/brand/list
gateway:
routes: #配置路由,可以有多个路由
# - id: member_routh01 # 路由id, 由程序员指定,保证唯一
# # 当前配置完成的需求说明:
# # 如果到网关的请求时 http://localhost:5050/commodity/brand/list ,gateway 通过断言。
# # 最终将请求路由转发到 http://localhost:9090/commodity/brand/list = >url=uri+path
# uri: http://localhost:9090
# predicates:
# - Path=/commodity/brand/list #断言,路径相匹配的进行路由
- id: raibnowsealiving_service_route # 路由id, 由程序员指定,保证唯一
# 当前配置完成的需求说明:
# 如果到网关的请求时 http://localhost:5050/api/service/???/??? ,gateway 通过断言。
# 最终将请求路由转发到 http://rainbowSealiving-service [注册到 nacosd renren-fast 服务ip+端口]/????? = >url=uri+path
# 因为我们要去掉断言到 Path的/api ,所以这里我们需要使用上路径重写。
uri: lb://rainbowsealiving-service
predicates:
- Path=/api/service/** #断言,路径相匹配的进行路由
filters:
# 也就是通过路径重写,最终的url 就是 http://localhost:7070
- RewritePath=/api/service/(?<segment>.*), /$\{segment}
- id: rainbowSealiving_commodity_route # 路由id, 由程序员指定,保证唯一
# 当前配置完成的需求说明:
# 如果到网关的请求时 http://localhost:5050/api/commodity/list/tree ,gateway 通过断言。
# 最终将请求路由转发到 http://rainbowSealiving-commodity [注册到 nacosd renren-fast 服务ip+端口]/????? = >url=uri+path
# 因为我们要去掉断言到 Path的/api ,所以这里我们需要使用上路径重写。
# 说明: /api/commodity/是一个更加精确的路径,必须将这组路由放在 /api/这里上
# 否则会报错
uri: lb://rainbowSealiving-commodity
predicates:
- Path=/api/commodity/** #断言,路径相匹配的进行路由
filters:
# 也就是通过路径重写,最终的url 就是 http://localhost:9090
- RewritePath=/api/(?<segment>.*), /$\{segment}
- id: rainbowsealiving_renren_fast_route # 路由id, 由程序员指定,保证唯一
# 当前配置完成的需求说明:
# 如果到网关的请求时 http://localhost:5050/api/???/??? ,gateway 通过断言。
# 最终将请求路由转发到 http://renren-fast [注册到 nacosd renren-fast 服务ip+端口]/????? = >url=uri+path
# 因为我们要去掉断言到 Path的/api ,所以这里我们需要使用上路径重写。
uri: lb://renren-fast
predicates:
- Path=/api/** #断言,路径相匹配的进行路由
filters:
# 也就是通过路径重写,最终的url 就是 http://localhost:8090
- RewritePath=/api/(?<segment>.*), /renren-fast/$\{segment}
# for nginx 增加一组路由
- id: hspliving_host_route
uri: lb://rainbowSealiving-commodityy
predicates:
- Host=**.rainbowsealiving.com
nacos:
discovery:
server-addr: 127.0.0.1:8848 #配置nacos地址
application:
name: rainbowsealiving-gateway
-
重启网关, 再访问,会依然错误, 因为 nginx 在转发请求到网关会丢掉一些信息,比如host,因此需要重新配置
-
再次修改 vi /mydata/nginx/conf/conf.d/rainbowsealiving.conf
shell
[root@localhost conf.d]# vi /mydata/nginx/conf/conf.d/rainbowsealiving.conf

properties
location / {
proxy_set_header Host $host;
proxy_pass http://rainbowsealiving;
}
- sudo docker restart nginx //重启 nginx
shell
[root@localhost conf.d]# sudo docker restart nginx

- 重启该模块项目,rainbowsealiving-gateway

host 的网关丢失,host 的优先级,注意事项
- 不要把 Host 路由配置到前面, 否则按照域名+api 方式的路由就不会成功了, 因为会优先匹配到 Host


- 将路由配置放在其它路由配置后面, 再测试就 OK 了

Nginx 的 <font style="color:rgb(0,0,0);">配置动静分离</font>

配置实现
- 在 Nginx 创建 static 目录


shell
[root@localhost html]# mkdir static
- 把后端项目的静态资源 ,E:\Java\project\RainbowSealiving\RainbowSealiving-commodity\src\main\resources\static目录下所有静态资源文件 , 上传到Nginx下的static目录 , 然后删除 E:\Java\project\RainbowSealiving\RainbowSealiving-commodity\src\main\resources\static下所有文件





-这时 rebuild ,然后重启 RainbowSealiving-commodity 模块, 访问 首页面, 会出现如下情况, 如果 还看到图片,是因为缓存原因, 可以换一个浏览器,或者禁用缓存。
正确的是,我们访问的结果应该是如下的,RainbowSealiving-commodity 模块项目是无法访问到我们配置到 Nginx 当中的静态文件资源的,因为我们并没有在 Nginx 当中配置该 static 静态文件的路由路径,所以项目是无法找到该 Nginx 当中的 /mydata/nginx/html/static 路径的静态资源的。


- 对 index.html 和 list.html 模板文件访问静态资源路径进行替换修改
IDEA 快捷键:选择要替换的文本内容,Ctrl + R

shell
\"index
\"/static/index

shell
\"search
\"/static/search

shell
\"\.\/search
\"/static/search
- 对 Nginx 进行配置,配置对 Nginx 下配置的 static 静态资源进行路由路径的配置
所 Nginx 配置路径,在如下位置:



properties
location /static/ {
root /usr/share/nginx/html;
}
location / {
proxy_set_header Host $host;
proxy_pass http://rainbowsealiving;
}
- 重启 Nginx
shell
[root@localhost conf.d]# docker restart nginx

- 重启 RainbowSealiving-commodity 服务, 这时访问 页面, 就正常了, 也实现了动静分离


配置首页点击分类 到检索页面
- 修改 Nginx 当中的 catalogLoader.js
该文件所在路径是在 <font style="color:rgb(0,0,0);">/mydata/nginx/html/static/index/js</font>



properties
$.each(ctg3List, function (i, ctg3) {
var cata3link = $("<a href=\"/list.html?catalog3Id=" + ctg3.id + "\" style=\"color: #999;\">" + ctg3.name + "</a>");
- 重启 Nginx 服务
shell
[root@localhost js]# docker restart nginx


最后:
"在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。"