图中红色框为区域模块,国内属于默认展示,其他的使用区域表存储,其对应的目的地可以有多个
图中黑色框为目的地模块,树型结构,数据库使用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;
private Integer ishot = STATE_NORMAL; private Integer seq; private String info;
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<>(); }
|
基础区域服务
分页查询
接口信息
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)); } }
|
主键查询
接口信息
Controller
RegionController@GetMapping("/detail") public R<Region> getById(Long id) { return R.ok(regionService.getById(id)); }
|
保存区域
接口信息
Controller
RegionController@PostMapping("/save") public R<?> save(Region region) { regionService.save(region); return R.ok(); }
|
更新区域
接口信息
Controller
RegionController@PostMapping("/update") public R<?> update(Region region) { regionService.updateById(region); return R.ok(); }
|
删除区域
接口信息
Controller
RegionController@PostMapping("/delete/{id}") public R<?> delete(@PathVariable Long id) { regionService.removeById(id); return R.ok(); }
|
基础目的地服务
查询所有
接口信息
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()); } }
|
主键查询
接口信息
Controller
DestinationController@GetMapping("/detail") public R<Destination> getById(Long id) { return R.ok(destinationService.getById(id)); }
|
保存目的地
接口信息
Controller
DestinationController@PostMapping("/save") public R<?> save(Destination dst) { destinationService.save(dst); return R.ok(); }
|
更新目的地
接口信息
Controller
DestinationController@PostMapping("/update") public R<?> update(Destination dst) { destinationService.updateById(dst); return R.ok(); }
|
删除目的地
接口信息
Controller
DestinationController@PostMapping("/delete/{id}") public R<?> delete(@PathVariable Long id) { destinationService.removeById(id); return R.ok(); }
|
热门区域查询
前端对应页面如下图中的红色框:
接口信息
Service
找到:RegionService,添加热门区域查询方法
RegionServicepublic interface RegionService extends IService<Region> {
List<Region> findHotList(); }
|
找到:RegionServiceImpl,实现上述方法
RegionServiceImpl@Service public class RegionServiceImpl extends ServiceImpl<RegionMapper, Region> implements RegionService {
@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查询区域目的地,包括目的地的子目的地
前端对应页面如下图中黑色框:
接口信息
Mapper
找到:DestinationMapper,添加查询方法:
DestinationMapperpublic 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
List<Destination> findHotList(Long rid);
|
找到:DestinationServiceImpl,实现上述方法
这里使用了三种实现方式:
1、自连接查询
2、循环查询,有N+1问题,即需要循环N次查询数据库
3、循环查询,但是使用多线程方式
线程池配置类:
@Configuration public class AppConfig {
@Bean public ThreadPoolExecutor bizThreadPoolExecutor() {
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; }
@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; }
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; }
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) { bizThreadPoolExecutor.execute(() -> { List<Destination> children = list(Wrappers.<Destination>lambdaQuery().eq(Destination::getParentId, destination.getId()).last("limit 10")); destination.setChildren(children); 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获取其对应的目的地,父级目的地,不包含其子目的地
接口信息
该接口输入后台管理接口
Service
找到:DestinationService,添加查区域目的地方法
public interface DestinationService extends IService<Destination> {
List<Destination> getDestinationByRegionId(Long regionId); }
|
找到:DestinationServiceImpl,实现上述方法
DestinationService
@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,即查询所有父目的地
接口信息
该接口输入后台管理接口
请求参数
请求参数除了基本参数,还包括分页参数,将公共的参数放到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
Page<Destination> pageList(DestinationQuery query);
|
找到:DestinationServiceImpl,实现上述方法
@Override public Page<Destination> pageList(DestinationQuery query) { LambdaQueryWrapper<Destination> wrapper = Wrappers.<Destination>lambdaQuery(); wrapper.isNull(query.getParentId() == null, Destination::getParentId); 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时,停止查询。
前端对应页面如下图红色框:
接口信息
Service
找到:DestinationService,添加查询吐司方法
DestinationService
List<Destination> toasts(Long destId);
|
找到:DestinationServiceImpl,实现上述方法
@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)); }
|