图中红色框为区域模块,国内属于默认展示,其他的使用区域表存储,其对应的目的地可以有多个
图中黑色框为目的地模块,树型结构,数据库使用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)); }
 
  |