搭建环境
- 前端部署到nginx中
server {
listen 9001;
server_name localhost;
location / {
root html/hm-mall-admin;
}
}
server {
listen 9002;
server_name localhost;
location / {
root html/hm-mall-portal;
}
}
- 启动nacos,创建gatway模块设置端口为10010的网关,将模块统一部署到nacos台中
server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes: # 网关路由配置
- id: user-service # 路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
- Path=/address/**
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
- Path=/pay/**
- id: item-service
uri: lb://itemservice
predicates:
- Path=/item/**
- id: search-service
uri: lb://searchservice
predicates:
- Path=/search/**
default-filters: # 默认过滤项
- AddRequestHeader=Truth, Itcast is freaking awesome!
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:9001"
- "http://localhost:9002"
- "http://127.0.0.1:9001"
- "http://127.0.0.1:9002"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期
商品业务(CRUD)
分页查询
根据MP提供的分页进行查询
1)在MP的官网查询分页插件,将最新版的cv到刚创建的config配置类中
2)在pageDTO封装类中定义两个属性:list和total
3)由于mp底层已经自动注入好Mapper和service层之间的关联,无需自动注入@Autowired,但是在每一个方法中都需要重写getBaseMapper(获取Mapper),但我目前使用的是自动注入Mapper
4)习惯分三层去写就没写到一块,所以在controller定义参数和返回参数类型的接口(GET)
5)路径为"/list",参数是前端给出的加注解@RequestParam,从request里面拿取值(http://localhost:8080/list?page=1&size=10),page和size就需要加@RequestParam
6)service层分实现类和接口类(解耦),实现层实现page(MP自带的),然后获取page提供的total(getTotal)和list(getRecords),将获取的结果封装到pageDTO
根据ID查询
1)创建参数为id、返回类型为实体类的接口(GET),路径为"/{id}"
http://localhost:8080/id,id就需要使用@PathVariable,相当于uri路径的占位符
2)去service层实现,调用Mapper层的方法selectById(id)
基于mp的其他查询(符合分类一致、价格范围的商品)
1)创建参数为category、minPrice、maxPrice(@RequestParam),返回类型为实体类的接口(GET)
2)去service层实现,调用条件查询方法query
- 根据id查询使用getById(id)
- 根据id查询多个listByIds(List)
- 根据复杂条件查询一个或者多个以及分页
- 一个:query().eq().ge().one()
- 多个:query().eq().ge().list()
- 分页:query().eq().ge().page(new Page(page,size))
新增商品
1)创建参数为实体类(@RequestBody)、返回类型为int或boolean的接口(请求头为POST)
@RequestBody:是用于前端传给后端的JSON格式的参数
2)service层调用Mapper层的方法insert(item)
商品的上架和下架
对商品进行修改或者删除,需要先对商品进行下架,修改完毕之后在对其下架
1)对商品上架、下架,只需要改变商品的状态
2)创建参数是id和status、返回类型是int或布尔的接口(PUT),路径:“/status/{id}/{status}”
3)service层调用Mapper层的方法updateById(item)
4)由于接收的是item实体类,需要封装对象先对其创建new,然后将前端改变的id和状态赋予对象
修改商品
1)创建参数为实体类、返回类型为int或boolean的接口(请求头为PUT)
2)service层调用Mapper层的方法updateById(item)
3)修改前需要回显数据,调用根据id查询信息的方法即可
删除商品
此处做的不是逻辑删除(没有真正意义上的删除,而是改变状态为不可用),而是从数据库直接删除一条数据
1)创建参数id、返回类型为int或boolean的接口(请求头为DELETE)
2)service层调用Mapper层的方法deleteById(id)
搜索业务(ES搜索引擎实现)
创建搜索微服务search-service,添加elasticsearch依赖、elasticsearch配置信息、注册elasticsearch的HighLevelRestClient对象,前面已经将search由网关管理
<!-- elasticsearch依赖 -->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--nacos服务注册发现依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--amqp mq依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
#elasticsearch配置信息
server:
port: 8084
spring:
application:
name: searchservice
cloud:
nacos:
server-addr: localhost:8848
rabbitmq:
host: localhost
port: 5672
virtual-host: /
username: itcast
password: 123321
logging:
level:
com.hmall: debug
pattern:
dateformat: HH:mm:ss:SSS
/** 注册elasticsearch的HighLevelRestClient对象 **/
@Configuration
public class ElasticsearchConfig {
@Bean
public RestHighLevelClient restHighLevelClient() {
return new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://localhost:9200")
));
}
}
设计索引库数据结构
基本字段包括:
- 用于关键字全文检索的字段,比如All,里面包含name、brand、category信息
- 用于自动补全(suggestion)的字段,包括brand、category信息
- 分类、品牌、价格、销量、id、name、评价数量、图片
#创建数据索引库
PUT /hmall
{
"settings": {
"analysis": {
"analyzer": {
"text_anlyzer": {
"tokenizer": "ik_max_word",
"filter": "py"
},
"completion_analyzer": {
"tokenizer": "keyword",
"filter": "py"
}
},
"filter": {
"py": {
"type": "pinyin",
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"remove_duplicated_term": true,
"none_chinese_pinyin_tokenize": false
}
}
}
},
"mappings": {
"properties": {
"id":{
"type": "keyword"
},
"name":{
"type": "text",
"analyzer": "text_anlyzer",
"search_analyzer": "ik_smart",
"copy_to": "all"
},
"image":{
"type": "keyword",
"index": false
},
"price":{
"type": "integer"
},
"category":{
"type": "keyword",
"copy_to": "{all}"
},
"brand":{
"type": "keyword",
"copy_to": "all"
},
"sold":{
"type": "integer"
},
"commentCount":{
"type": "integer"
},
"isAD":{
"type": "boolean"
},
"all":{
"type": "text",
"analyzer": "text_anlyzer",
"search_analyzer": "ik_smart"
},
"suggestion":{
"type": "completion",
"analyzer": "completion_analyzer"
}
}
}
}
完成数据批量导入
1)将商品微服务中的分页查询商品接口定义为一个FeignClient,放到feign-api模块中,此项目只暴露了分页查询接口和查询根据id查询(数据同步中的上架监听)
2)搜索服务编写一个业务,实现下面功能:
- 调用item-service提供的FeignClient(itemClient),分页查询商品 PageDTO,itemClient.list
- 将查询到的商品封装为一个ItemDoc对象,放入ItemDoc集合
- 将ItemDoc集合批量导入elasticsearch中
多线程实现批量导入,由于数据库中的数据过多,采用分页批量导入
@SpringBootTest
@RunWith(SpringRunner.class)
@Slf4j
public class InjectData {
@Autowired
private RestHighLevelClient esClient;
@Autowired
private ItemClient itemClient;
//多线程导入es
@Test
public void injectDataByThread() throws IOException {
//创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 60,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(3),
new ThreadPoolExecutor.DiscardOldestPolicy());
//查询总条数
Integer total = itemClient.list(1,20).getTotal().intValue();
//计算总页数
int pages = (int) Math.ceil((double) total / 5500);
//分页导入数据
for (int page = 1; page <= pages; page++) {
PageDTO<Item> itemPageDTO = itemClient.list(page, 3000);
if (itemPageDTO.getList() != null && itemPageDTO.getList().size() > 0) {
threadPool.execute(new Runnable() {
@Override
public void run() {
//加入到线程池任务
try {
// List<User> list = userService.list();
// 1.创建Request
BulkRequest request = new BulkRequest();
// 2.准备参数,添加多个新增的Request
for (Item user : itemPageDTO.getList()) {
// 2.1.转换为文档类型HotelDoc
ItemDoc userDoc = new ItemDoc(user);
// 2.2.创建新增文档的Request对象
request.add(new IndexRequest("hmall")
.id(userDoc.getId().toString())
.source(JSON.toJSONString(userDoc), XContentType.JSON));
}
// 3.发送请求
esClient.bulk(request, RequestOptions.DEFAULT);
log.info("【es】thread:{
},msg:本次同步 {
} 条数据",
Thread.currentThread().getName(), itemPageDTO.getList().size());
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
try {
//延迟3秒等elasticsearch完成写入数据
Thread.sleep(3000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
实现搜索栏自动补全功能(suggestion)
1)创建参数为 key: 用户输入的词条前缀、返回值List:自动补全的词条集合的接口(GET),路径:/search/suggestion
2)service层实现自动补全功能,suggestion是在实体类中拼接brand和category,source是查询商品的信息