图中红色框为区域模块,国内属于默认展示,其他的使用区域表存储,其对应的目的地可以有多个

图中黑色框为目的地模块,树型结构,数据库使用parent_id来维护关系。

数据库

目的地服务分为两个模块:区域和目的地。区域和目的地是1对多,为了方便使用,这里关系交给1方来维护,打破了第一范式

国内区域设计为默认区域,即不存入数据库。

地区表

其中ref_ids用来存其关联的目的地

CREATE TABLE `region` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`sn` varchar(255) DEFAULT NULL,
`ishot` bit(1) DEFAULT NULL,
`seq` int DEFAULT NULL,
`info` varchar(255) DEFAULT NULL,
`ref_ids` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC;

目的地表

目的地为树型结构,使用parent_id来关联关系

CREATE TABLE `destination` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`english` varchar(255) DEFAULT NULL,
`cover_url` varchar(255) DEFAULT NULL,
`info` varchar(255) DEFAULT NULL,
`parent_name` varchar(255) DEFAULT NULL,
`parent_id` bigint DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=607 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC;

实体类

找到模块:trip-article-api,找到包com.swx.article.domain,创建地区实体类

Region
/**
* 区域对象
*/
@Getter
@Setter
@TableName("region")
public class Region implements Serializable {

public static final int STATE_HOT = 1;
public static final int STATE_NORMAL = 0;

@TableId(type = IdType.AUTO)
private Long id;

private String name; // 地区名
private String sn; // 地区编码
private String refIds; // 关联id,多个以,隔开

private Integer ishot = STATE_NORMAL;
private Integer seq; // 序号
private String info; // 简介

/**
* 解析子目的地
*
* @return 子目的地
*/
public List<Long> parseRefIds() {
ArrayList<Long> ids = new ArrayList<>();
if (StringUtils.hasLength(refIds)) {
String[] split = refIds.split(",");
if (split.length > 0) {
for (int i = 0; i < split.length; i++) {
ids.add(Long.parseLong(split[i]));

}
}
}
return ids;
}
}

目的地实体类

Destination
/**
* 目的地(行政地区:国家/省份/城市)
*/
@Getter
@Setter
@TableName("destination")
public class Destination {

@TableId(type = IdType.AUTO)
private Long id;
private String name;
private String english;
private Long parentId;
private String parentName;
private String info;
private String coverUrl;
@TableField(exist = false)
private List<Destination> children = new ArrayList<>();
}

基础区域服务

分页查询

接口信息

路径地址 http://localhost:9000/article/regions
请求方式 GET
请求参数 Page
返回结果 Page

Controller

RegionController
@RestController
@RequestMapping("/regions")
public class RegionController {

private final RegionService regionService;

public RegionController(RegionService regionService) {
this.regionService = regionService;
}

@GetMapping
public R<Page<Region>> pageList(Page<Region> page) {
return R.ok(regionService.page(page));
}
}

主键查询

接口信息

路径地址 http://localhost:9000/article/regions/detail
请求方式 GET
请求参数 id
返回结果 Region

Controller

RegionController
@GetMapping("/detail")
public R<Region> getById(Long id) {
return R.ok(regionService.getById(id));
}

保存区域

接口信息

路径地址 http://localhost:9000/article/regions/save
请求方式 POST
请求参数 Region
返回结果

Controller

RegionController
@PostMapping("/save")
public R<?> save(Region region) {
regionService.save(region);
return R.ok();
}

更新区域

接口信息

路径地址 http://localhost:9000/article/regions/update
请求方式 POST
请求参数 Region
返回结果

Controller

RegionController
@PostMapping("/update")
public R<?> update(Region region) {
regionService.updateById(region);
return R.ok();
}

删除区域

接口信息

路径地址 http://localhost:9000/article/regions/delete/{id}
请求方式 POST
请求参数 id
返回结果

Controller

RegionController
@PostMapping("/delete/{id}")
public R<?> delete(@PathVariable Long id) {
regionService.removeById(id);
return R.ok();
}

基础目的地服务

查询所有

接口信息

路径地址 http://localhost:9000/article/destinations/list
请求方式 GET
请求参数
返回结果 List

Controller

DestinationController
@RestController
@RequestMapping("/destinations")
public class DestinationController {

private final DestinationService destinationService;

public DestinationController(DestinationService destinationService) {
this.destinationService = destinationService;
}

@GetMapping("/list")
public R<List<Destination>> listAll() {
return R.ok(destinationService.list());
}
}

主键查询

接口信息

路径地址 http://localhost:9000/article/destinations/detail
请求方式 GET
请求参数 id
返回结果 Destination

Controller

DestinationController
@GetMapping("/detail")
public R<Destination> getById(Long id) {
return R.ok(destinationService.getById(id));
}

保存目的地

接口信息

路径地址 http://localhost:9000/article/destinations/save
请求方式 POST
请求参数 Region
返回结果

Controller

DestinationController
@PostMapping("/save")
public R<?> save(Destination dst) {
destinationService.save(dst);
return R.ok();
}

更新目的地

接口信息

路径地址 http://localhost:9000/article/destinations/update
请求方式 POST
请求参数 Destination
返回结果

Controller

DestinationController
@PostMapping("/update")
public R<?> update(Destination dst) {
destinationService.updateById(dst);
return R.ok();
}

删除目的地

接口信息

路径地址 http://localhost:9000/article/destinations/delete/{id}
请求方式 POST
请求参数 id
返回结果

Controller

DestinationController
@PostMapping("/delete/{id}")
public R<?> delete(@PathVariable Long id) {
destinationService.removeById(id);
return R.ok();
}

热门区域查询

前端对应页面如下图中的红色框:

接口信息

路径地址 http://localhost:9000/article/regions/hotList
请求方式 GET
请求参数
返回结果 List

Service

找到:RegionService,添加热门区域查询方法

RegionService
public interface RegionService extends IService<Region> {
/**
* 热门区域查询
* @return 热门区域
*/
List<Region> findHotList();
}

找到:RegionServiceImpl,实现上述方法

RegionServiceImpl
@Service
public class RegionServiceImpl extends ServiceImpl<RegionMapper, Region> implements RegionService {

/**
* 热门区域查询
*
* @return 热门区域
*/
@Override
public List<Region> findHotList() {
return list(Wrappers.<Region>lambdaQuery()
.eq(Region::getIshot, Region.STATE_HOT)
.orderByAsc(Region::getSeq));
}
}

Controller

找到:RegionController,添加热门区域查询方法

RegionController
@GetMapping("/hotList")
public R<List<Region>> hotList() {
return R.ok(regionService.findHotList());
}

热门区域目的地

根据热门区域ID查询区域目的地,包括目的地的子目的地

前端对应页面如下图中黑色框:

接口信息

路径地址 http://localhost:9000/article/destinations/hotList
请求方式 GET
请求参数
返回结果 List

Mapper

找到:DestinationMapper,添加查询方法:

DestinationMapper
public interface DestinationMapper extends BaseMapper<Destination> {

List<Destination> selectHotListByRid(@Param("rid") Long rid, @Param("ids") List<Long> ids);
}

找到:DestinationMapper.xml,实现实现上述方法

DestinationMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.swx.article.mapper.DestinationMapper">

<resultMap id="FullResultMap" type="com.swx.article.domain.Destination">
<id property="id" column="id" />
<result property="name" column="name" />
<collection property="children" ofType="com.swx.article.domain.Destination" columnPrefix="c_">
<id property="id" column="id" />
<result property="name" column="name" />
</collection>
</resultMap>

<select id="selectHotListByRid" resultMap="FullResultMap">
SELECT province.id, province.name, city.id c_id, city.name c_name
FROM destination province LEFT JOIN destination city ON province.id = city.parent_id
<where>
<if test="rid == -1">
province.parent_id = 1
</if>
<if test="rid > 0">
province.id in
<foreach collection="ids" open="(" separator="," close=")" item="id">
#{id}
</foreach>
</if>
</where>
ORDER BY c_id
</select>
</mapper>

Service

找到:DestinationService,添加热门区域下目的地查询方法

DestinationService
/**
* 根据热门区域ID查询热门目的地
* @param rid 区域ID
* @return 热门目的地
*/
List<Destination> findHotList(Long rid);

找到:DestinationServiceImpl,实现上述方法

这里使用了三种实现方式:

1、自连接查询
2、循环查询,有N+1问题,即需要循环N次查询数据库
3、循环查询,但是使用多线程方式

线程池配置类:

@Configuration
public class AppConfig {

@Bean
public ThreadPoolExecutor bizThreadPoolExecutor() {
// 创建线程池的方式
/*
1. Executors 创建,不推荐,
默认创建的工作队列,使用的是 LinkedBlockingDeque 队列,且默认容量为 Integer 的最大值
工作队列的容量过大,会导致核心线程工作过载,对垒中任务数过多,且非核心线程无法参与处理,最终导致内存溢出
Executors.newCachedThreadPool(50);
*/

// 2. 直接new ThreadPoolExecutor 对象 推荐
return new ThreadPoolExecutor(10, 50, 10, TimeUnit.SECONDS,
new LinkedBlockingDeque<>(100));
}
}
DestinationServiceImpl
@Service
public class DestinationServiceImpl extends ServiceImpl<DestinationMapper, Destination> implements DestinationService {

private final RegionService regionService;
private final ThreadPoolExecutor bizThreadPoolExecutor;

public DestinationServiceImpl(RegionService regionService, ThreadPoolExecutor bizThreadPoolExecutor) {
this.regionService = regionService;
this.bizThreadPoolExecutor = bizThreadPoolExecutor;
}

/**
* 根据热门区域ID查询热门目的地
*
* @param rid 区域ID
* @return 热门目的地
*/
@Override
public List<Destination> findHotList(Long rid) {
List<Destination> destinations = null;
if (rid < 0) {
destinations = this.baseMapper.selectHotListByRid(rid, null);
} else {
Region region = regionService.getById(rid);
if (region == null) {
return Collections.emptyList();
}
List<Long> ids = region.parseRefIds();
destinations = this.baseMapper.selectHotListByRid(rid, ids);
}
for (Destination destination : destinations) {
List<Destination> children = destination.getChildren();
if (children == null) {
continue;
}
destination.setChildren(children.stream().limit(10).collect(Collectors.toList()));
}
return destinations;
}

/**
* 使用代码循环方式,有N+1问题
*
* @param rid 区域ID
* @return 热门目的地
*/
public List<Destination> findHostListFor(Long rid) {
List<Destination> destinations = null;
LambdaQueryWrapper<Destination> wrapper = new LambdaQueryWrapper<>();
if (rid < 0) {
destinations = list(wrapper.eq(Destination::getParentId, 1));
} else {
destinations = this.getDestinationByRegionId(rid);
}

for (Destination destination : destinations) {
// 清楚之前的条件
wrapper.clear();
List<Destination> children = list(wrapper.eq(Destination::getParentId, destination.getId()).last("limit 10"));
destination.setChildren(children);
}
return destinations;
}

/**
* 使用代码循环方式,有N+1问题
* 使用多线程,同时发查询请求
*
* @param rid 区域ID
* @return 热门目的地
*/
public List<Destination> findHostListThread(Long rid) {
List<Destination> destinations = null;
LambdaQueryWrapper<Destination> wrapper = new LambdaQueryWrapper<>();
if (rid < 0) {
destinations = list(wrapper.eq(Destination::getParentId, 1));
} else {
destinations = this.getDestinationByRegionId(rid);
}
// 如何等待所有异步线程结束,主线程再执行
CountDownLatch latch = new CountDownLatch(destinations.size());
for (Destination destination : destinations) {
// submit有返回值,且支持Callable,execute没有返回值,只支持Runnable
bizThreadPoolExecutor.execute(() -> {
// 清楚之前的条件
List<Destination> children = list(Wrappers.<Destination>lambdaQuery().eq(Destination::getParentId, destination.getId()).last("limit 10"));
destination.setChildren(children);
// 倒计时数量-1
latch.countDown();
});
}
// 返回结果前阻塞等待
try {
latch.await(10, TimeUnit.MINUTES);
} catch (InterruptedException e) {
e.printStackTrace();
}
return destinations;
}
}

Controller

找到:DestinationController,添加热门区域查询方法

DestinationController
@GetMapping("/hotList")
public R<List<Destination>> hotList(Long rid) {
return R.ok(destinationService.findHotList(rid));
}

查区域目的地

根据区域的ID获取其对应的目的地,父级目的地,不包含其子目的地

接口信息

该接口输入后台管理接口

路径地址 http://localhost:9000/article/regions/{id}/destination
请求方式 GET
请求参数 id
返回结果 List

Service

找到:DestinationService,添加查区域目的地方法

public interface DestinationService extends IService<Destination> {
/**
* 根据区域ID获取目的地
*
* @param regionId 区域ID
*/
List<Destination> getDestinationByRegionId(Long regionId);
}

找到:DestinationServiceImpl,实现上述方法

DestinationService
/**
* 根据区域ID获取目的地
*
* @param regionId 区域ID
*/
@Override
public List<Destination> getDestinationByRegionId(Long regionId) {
Region region = regionService.getById(regionId);
if (region == null) {
return Collections.emptyList();
}
List<Long> ids = region.parseRefIds();
if (ids.isEmpty()) {
return Collections.emptyList();
}
return listByIds(ids);
}

Controller

找到:RegionController,添加热门区域查询方法

RegionController
@RestController
@RequestMapping("/regions")
public class RegionController {

private final RegionService regionService;
private final DestinationService destinationService;

public RegionController(RegionService regionService, DestinationService destinationService) {
this.regionService = regionService;
this.destinationService = destinationService;
}

@GetMapping("/{id}/destination")
public R<List<Destination>> getDestination(@PathVariable Long id) {
return R.ok(destinationService.getDestinationByRegionId(id));
}
}

分页查询目的地

根据目的地的父ID获取其对应的目的地,父ID为NULL,即查询所有父目的地

接口信息

该接口输入后台管理接口

路径地址 http://localhost:9000/article/regions
请求方式 GET
请求参数 DestinationQuery
返回结果 Page

请求参数

请求参数除了基本参数,还包括分页参数,将公共的参数放到QueryObject中:

在 core 模块中创建包:com.swx.common.core.qo,该包下创建 QueryObject

QueryObject
@Getter
@Setter
@NoArgsConstructor
public class QueryObject {
private String keyword;
private Integer current = 1;
private Integer size = 10;

public QueryObject(Integer current, Integer size) {
this.current = current;
this.size = size;
}

public Integer getOffset() {
return (current - 1) * size;
}

}
@Getter
@Setter
public class DestinationQuery extends QueryObject {
private Long parentId;
}

Service

找到:DestinationService,添加分页查询方法

DestinationService
/**
* 分页查询
*
* @param query 查询参数
* @return 分页数据
*/
Page<Destination> pageList(DestinationQuery query);

找到:DestinationServiceImpl,实现上述方法

/**
* 分页查询
*
* @param query 查询参数
* @return 分页数据
*/
@Override
public Page<Destination> pageList(DestinationQuery query) {
LambdaQueryWrapper<Destination> wrapper = Wrappers.<Destination>lambdaQuery();
// parentId 为 null,查询所有 parent_id IS NULL 的数据
wrapper.isNull(query.getParentId() == null, Destination::getParentId);
// parentId 不为 null,根据 parent_id 查询
wrapper.eq(query.getParentId() != null, Destination::getParentId, query.getParentId());
// 关键字查询
wrapper.like(StringUtils.hasText(query.getKeyword()), Destination::getName, query.getKeyword());
return super.page(new Page<>(query.getCurrent(), query.getSize()), wrapper);
}

Controller

找到:DestinationController,添加分页查询方法

DestinationController
@GetMapping
public R<Page<Destination>> pageList(DestinationQuery query) {
return R.ok(destinationService.pageList(query));
}

目的地吐司查询

根据当前目的地ID,往上查询其父目的地,当父目的地的parent_id为NULL时,停止查询。

前端对应页面如下图红色框:

接口信息

路径地址 http://localhost:9000/article/regions/toasts
请求方式 GET
请求参数 destId
返回结果 List

Service

找到:DestinationService,添加查询吐司方法

DestinationService
/**
* 根据当前ID查询面包屑
* @param destId 当前目的地ID
* @return 面包屑
*/
List<Destination> toasts(Long destId);

找到:DestinationServiceImpl,实现上述方法

/**
* 根据当前ID查询面包屑
*
* @param destId 当前目的地ID
*/
@Override
public List<Destination> toasts(Long destId) {
ArrayList<Destination> toasts = new ArrayList<>();
while (destId != null) {
Destination dest = super.getById(destId);
if (dest == null) {
break;
}
toasts.add(dest);
destId = dest.getParentId();
}
// 逆序,方便前端展示
Collections.reverse(toasts);
return toasts;
}

Controller

找到:DestinationController,添加分页查询方法

DestinationController
@GetMapping("/toasts")
public R<List<Destination>> toasts(Long destId) {
return R.ok(destinationService.toasts(destId));
}