第十九章:过滤条件的筛选和商品详情后台实现
此博客用于个人学习,来源于网上,对知识点进行一个整理。
1. 过滤条件的筛选:
当我们点击页面的过滤项,要做哪些事情?
- 把过滤条件保存在 search 对象中(watch 监控到 search 变化后就会发送到后台)
- 在页面顶部展示已选择的过滤项
- 把商品分类展示到顶部面包屑
1.1 保存过滤项:
1)定义属性:
我们把已选择的过滤项保存在 search 中:
要注意,在 created 构造函数中会对 search 进行初始化,所以要在构造函数中对 filter 进行初始化:
search.filter 是一个对象,结构:
{
"过滤项名":"过滤项值"
}
2) 绑定点击事件:
给所有的过滤项绑定点击事件:
要注意,点击事件传2个参数:
- k:过滤项的 key
- option:当前过滤项对象
在点击事件中,保存过滤项到 selectedFilter :
selectFilter(k, o){
const obj = {};
Object.assign(obj, this.search);
if(k === '分类' || k === '品牌'){
o = o.id;
}
obj.filter[k] = o.name || o;
this.search = obj;
}
另外,这里 search 对象中嵌套了 filter 对象,请求参数格式化时需要进行特殊处理,修改 common.js 中的一段代码:
1.2 后台添加过滤条件:
既然请求已经发送到了后台,那接下来我们就在后台去添加这些条件:
1)拓展请求对象:
我们需要在请求类: SearchRequest 中添加属性,接收过滤属性。过滤属性都是键值对格式,但是 key 不确定,所以用一个 map 来接收即可。
public class SearchRequest {
private String key;// 搜索条件
private Integer page;// 当前页
private Map<String,Object> filter;//过滤条件
private static final Integer DEFAULT_SIZE = 20;// 每页大小,不从页面接收,而是固定大小
private static final Integer DEFAULT_PAGE = 1;// 默认页
public String getKey() {
return key;
}
public Map<String, Object> getFilter() {
return filter;
}
public void setFilter(Map<String, Object> filter) {
this.filter = filter;
}
public void setKey(String key) {
this.key = key;
}
public Integer getPage() {
if(page == null){
return DEFAULT_PAGE;
}
// 获取页码时做一些校验,不能小于1
return Math.max(DEFAULT_PAGE, page);
}
public void setPage(Integer page) {
this.page = page;
}
public Integer getSize() {
return DEFAULT_SIZE;
}
}
2)添加过滤条件:
目前,我们的基本查询是这样的:
现在,我们要把页面传递的过滤条件也加入进去。
因此不能在使用普通的查询,而是要用到 BooleanQuery,基本结构是这样的:
GET /heima/_search
{
"query":{
"bool":{
"must":{ "match": { "title": "小米手机",operator:"and"}},
"filter":{
"range":{"price":{"gt":2000.00,"lt":3800.00}}
}
}
}
}
所以,我们对原来的基本查询进行改造,因为比较复杂,我们将其封装到一个方法中:
/**
* 构建布尔查询
* @param request
* @return
*/
private BoolQueryBuilder buildBoolQueryBuilder(SearchRequest request) {
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
//给布尔值添加基本查询条件
boolQueryBuilder.must(QueryBuilders.matchQuery("all",request.getKey()).operator(Operator.AND));
//添加过滤条件
//获取用户选择的过滤信息
Map<String,Object> filter = request.getFilter();
for (Map.Entry<String,Object> entry: filter.entrySet()){
String key = entry.getKey();
if (StringUtils.equals("品牌",key)){
key = "brandId";
}else if (StringUtils.equals("分类",key)){
key = "cid3";
}else {
key = "specs." + key + ".keyword";
}
boolQueryBuilder.filter(QueryBuilders.termQuery(key,entry.getValue()));
}
return boolQueryBuilder;
}
2. 商品详情后台实现:
当用户搜索到商品,肯定会点击查看,就会进入商品详情页,接下来我们完成商品详情页的后台部分。
2.1 商品详情页服务:
商品详情浏览量比较大,并发高,我们会独立开启一个微服务,用来展示商品详情。
1)创建 module:商品的详情页服务,命名为: leyou-goods-web。
2)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">
<parent>
<artifactId>leyou</artifactId>
<groupId>com.leyou.parent</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.leyou.goods</groupId>
<artifactId>leyou-goods-web</artifactId>
<version>1.0.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>com.leyou.item</groupId>
<artifactId>leyou-item-interface</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
3)编写启动类:
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class LeyouGoodsWebApplication {
public static void main(String[] args) {
SpringApplication.run(LeyouGoodsWebApplication.class, args);
}
}
4)application.yml 文件:
server:
port: 8084
spring:
application:
name: goods-web
thymeleaf:
cache: false
rabbitmq:
host: 192.168.56.101
virtual-host: /leyou
username: leyou
password: leyou
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
registry-fetch-interval-seconds: 5
5)页面模板:
从 leyou-portal 中复制 item.html 模板到当前项目 resource 目录下的 templates 中。
2.2 页面跳转:
1)修改页面跳转路径:
首先我们需要修改搜索结果页的商品地址,目前所有商品的地址都是:http://www.leyou.com/item.html。
我们应该跳转到对应的商品的详情页才对。
通过详情页的预览,我们知道商品详情页是多个 SKU 的集合,即 SPU。所以,页面跳转时,我们应该携带 SPU 的 id 信息。
采用了路径占位符的方式来传递 spu 的 id,我们打开 search.html,修改其中的商品路径:
2)nginx 反向代理:
接下来,我们要把这个地址指向我们刚刚创建的服务:leyou-goods-web,其端口为8084。在 nginx.conf 中添加一段逻辑:
把以 /item 开头的请求,代理到我们的8084端口。
3)编写跳转 controller:
在 leyou-goods-web 中编写 controller,接收请求,并跳转到商品详情页:
@Controller
@RequestMapping("item")
public class GoodsController {
/**
* 跳转到商品详情页
* @param model
* @param id
* @return
*/
@GetMapping("{id}.html")
public String toItemPage(Model model, @PathVariable("id")Long id){
return "item";
}
}
2.3 封装模型数据:
我们已知的条件是传递来的 spu 的 id,我们需要根据 spu 的 id 查询到下面的数据:
- spu 信息
- spu 的详情
- spu 下的所有 sku
- 品牌
- 商品三级分类
- 商品规格参数、规格参数组
1)商品微服务提供接口:
以上所需数据中,根据 id 查询 spu 的接口目前还没有,我们需要在商品微服务中提供这个接口:
GoodsApi:
/**
* 根据spu的id查询spu
* @param id
* @return
*/
@GetMapping("spu/{id}")
public Spu querySpuById(@PathVariable("id") Long id);
GoodsController:
@GetMapping("spu/{id}")
public ResponseEntity<Spu> querySpuById(@PathVariable("id") Long id){
Spu spu = this.goodsService.querySpuById(id);
if(spu == null){
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return ResponseEntity.ok(spu);
}
GoodsService:
public Spu querySpuById(Long id) {
return this.spuMapper.selectByPrimaryKey(id);
}
我们在页面展示规格时,需要按组展示:
组内有多个参数,为了方便展示。我们在 leyou-item-service 中提供一个接口,查询规格组,同时在规格组内的所有参数。
拓展 SpecGroup 类:我们在 SpecGroup 中添加一个 SpecParam 的集合,保存该组下所有规格参数。
@Table(name = "tb_spec_group")
public class SpecGroup {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long cid;
private String name;
//忽略该字段
@Transient
private List<SpecParam> params;// 该组下的所有规格参数集合
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getCid() {
return cid;
}
public void setCid(Long cid) {
this.cid = cid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<SpecParam> getParams() {
return params;
}
public void setParams(List<SpecParam> params) {
this.params = params;
}
}
然后提供查询接口:
SpecificationAPI:
@RequestMapping("spec")
public interface SpecificationApi {
/**
* 根据条件查询规格参数
* @param gid
* @return
*/
@GetMapping("params")
public List<SpecParam> queryParams(
@RequestParam(value = "gid",required = false)Long gid,
@RequestParam(value = "cid",required = false)Long cid,
@RequestParam(value = "generic",required = false)Boolean generic,
@RequestParam(value = "searching",required = false)Boolean searching
);
@GetMapping("group/param/{cid}")
public List<SpecGroup> queryGroupsWithParam(@PathVariable("cid")Long cid);
}
SpecificationController:
@GetMapping("group/param/{cid}")
public ResponseEntity<List<SpecGroup>> queryGroupsWithParam(@PathVariable("cid")Long cid){
List<SpecGroup> groups = this.specificationService.queryGroupsWithParam(cid);
if (CollectionUtils.isEmpty(groups)){
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(groups);
}
SpecificationService:
public List<SpecGroup> queryGroupsWithParam(Long cid) {
List<SpecGroup> groups = queryGroupsByCid(cid);
groups.forEach(group->{
List<SpecParam> params = this.queryParamsByGid(group.getId(), null, null, null);
group.setParams(params);
});
return groups;
}
在 service 中,我们调用之前编写过的方法,查询规格组,和规格参数,然后封装返回。
2) 创建 FeignClient:
我们在 leyou-goods-web 服务中,创建 FeignClient:
BrandClient:
@FeignClient("item-service")
public interface BrandClient extends BrandApi {
}
CategoryClient:
@FeignClient("item-service")
public interface CategoryClient extends CategoryApi {
}
GoodsClient:
@FeignClient("item-service")
public interface GoodsClient extends GoodsApi {
}
SpecificationClient:
@FeignClient(value = "item-service")
public interface SpecificationClient extends SpecificationApi{
}
3)封装数据模型:
我们创建一个 GoodsService,在里面来封装数据模型。
这里要查询的数据:
-
SPU
-
SpuDetail
-
SKU 集合
-
商品分类
- 这里值需要分类的 id 和 name 就够了,因此我们查询到以后自己需要封装数据
-
品牌对象
-
规格组
- 查询规格组的时候,把规格组下所有的参数也一并查出,上面提供的接口中已经实现该功能,我们直接调用
-
sku 的特有规格参数
- 在页面渲染时,需要知道参数的名称
我们就需要把 id 和 name一一对应起来,因此需要额外查询 sku 的特有规格参数,然后变成一个 id:name 的键值对格式,也就是一个 Map,方便将来根据 id 查找。
GoodsService:
@Service
public class GoodsService {
@Autowired
private BrandClient brandClient;
@Autowired
private CategoryClient categoryClient;
@Autowired
private GoodsClient goodsClient;
@Autowired
private SpecificationClient specificationClient;
public Map<String,Object> loadData(Long spuId){
Map<String,Object> model = new HashMap<>();
//根据spuId查询spu
Spu spu = this.goodsClient.querySpuById(spuId);
//查询spuDetail
SpuDetail spuDetail = this.goodsClient.querySpuDetailBySpuId(spuId);
//查询分类,Map<String,Object>
List<Long> cids = Arrays.asList(spu.getCid1(),spu.getCid2(),spu.getCid3());
List<String> names = this.categoryClient.queryNamesByIds(cids);
//初始化一个分类的map
List<Map<String,Object>> categories = new ArrayList<>();
for (int i = 0;i < cids.size();i++){
Map<String,Object> map = new HashMap<>();
map.put("id",cids.get(i));
map.put("name",names.get(i));
categories.add(map);
}
//查询品牌
Brand brand = this.brandClient.queryBrandById(spuId);
//查询skus
List<Sku> skus = this.goodsClient.querySkusBySpuId(spuId);
//查询规格参数组
List<SpecGroup> groups = this.specificationClient.queryGroupsWithParam(spu.getCid3());
//查询特殊的规格参数
List<SpecParam> params = this.specificationClient.queryParams(null,spu.getCid3(),null,null);
//初始化特殊规格参数的map
Map<Long,String> paramMap = new HashMap<>();
params.forEach(param->{
paramMap.put(param.getId(),param.getName());
});
model.put("spu",spu);
model.put("spuDetail",spuDetail);
model.put("categories",categories);
model.put("brand",brand);
model.put("skus",skus);
model.put("groups",groups);
model.put("paramMap",paramMap);
return model;
}
}
然后在 controller 中把数据放入 model:
@Controller
@RequestMapping("item")
public class GoodsController {
@Autowired
private GoodsService goodsService;
/**
* 跳转到商品详情页
* @param model
* @param id
* @return
*/
@GetMapping("{id}.html")
public String toItemPage(Model model, @PathVariable("id")Long id){
// 加载所需的数据
Map<String, Object> modelMap = this.goodsService.loadModel(id);
// 放入模型
model.addAllAttributes(modelMap);
return "item";
}
}