目的地页功能
需求分析
1.完成热门目的地的两级目录的显示
2.完成单个地点的吐司条显示
3.完成单个地点的概况显示
4.完成单个地点的热门攻略显示
第一部分(完成热门目的地的两级目录的显示)
来看这个需求,首先需要展示热门目的地,然后展示两级目录,这就是基本的思路,成品图如下
![image]()
不去看代码就单纯看这个图,第一层,我们需要查出热点地区,地区有很多,热点地区就那么几个,所以就需要一个标记是否为热点的一个字段,以这个字段为基础来查询,按F12去看看接口
![image]()
以这个接口为基,我们来去编写后端,根据条件,Get方法,接口是/regions,返回值就是这部分对应的表,表明简单粗暴就是地区表(region),实体类也叫这个名字,返回的并非一条数据,所以选择R<List< Region >>作为返回值,这样就完成了一个controller接口
@GetMapping("/regions")
public R<List<Region>> regions(Long ishot) {
return R.ok(regionService.queryRegion(ishot));
}
然后去实现查询的方法,根据是的字段去查询即可,若依的框架允许在本类的方法里直接使用lambdaQuery()来进行条件构造,若不是本类的话,必须重新new一个条件构造器,这里从参数列表就能看出来,isHot是作为查询的条件,这里是在本类,只需要简单的条件构造器就好了,代码如下
public List<Region> queryRegion(Long ishot) {
return lambdaQuery()
.eq(Region::getIshot, ishot)
.orderByAsc(Region::getSeq)
.list();
}
此时再去前端就可以看见热点地区了,然后就是两级目录的显示,热点地区的下一级就是目的地了,用到的Destination的相关部分,去看看前端的接口
![image]()
方法是Get方法,接口是/destinations中的/search方法传了一个RegionId,那就是用刚才的热点地区的Id去进行目的地查询了,返回一个List< Destination>对象就ok了,controller代码如下
@GetMapping("/search")
public R<List<Destination>> search(Long regionId) {
return R.ok(destinationService.queryDestination(regionId));
}
接着去实现queryDestination,这里我们对中国,咱们将regionId设置为了-1,但是parentId统一设置为了1,所以需要与其他Region进行区分书写,也就是分为了regionId == -1 与else的情况,至于这么写是因为数据库当初设计的时候,中国就是必须为热点地区(毕竟咱是中国人),然后其他国家对应的,目的地id关联id设置的时候,并没有满足原子性,设置成了数组,直接把所有一级目录的id写进去了,所以这里才需要如此区分,首先是中国部分
List<Destination> list = null;
if (regionId == -1) {
list = lambdaQuery()
.eq(Destination::getParentId, 1)
.list();
}
初始化了一个列表用来承载这两个信息,然后使用条件构造器,取ParentId==1的记录条目,放进list里,然后是其他国家的部分,由于regionId写成了数组,中间是有逗号分隔的,所以需要去split去进行分割,分割后进行遍历,每次得到一个id,用这个id来进行查询
Region region = regionMapper.selectById(regionId);
String refIds = region.getRefIds();
String[] split = refIds.split(",");
list = new ArrayList<>();
for (String s : split) {
Long id = Long.valueOf(s);
Destination destination = lambdaQuery().eq(Destination::getId, id).one();
list.add(destination);
}
再放进list,得到的这个就是一级目录的全部内容
关于二级目录,前端使用了一个children数组去接,类型是Destination(目的地)类型的,在上面我们已经得到了所有一级目录的数据条目,我们把这些数据条目中的Id作为parentId(父目录id)就可以查询到所有的二级目录条目了,当然,list是需要遍历的,需要写一个循环结构就ok了
or (Destination destination : list) {
Long id = destination.getId();
List<Destination> children = lambdaQuery()
.eq(Destination::getParentId, id)
.last("limit 10")
.list();
destination.setChildren(children);
}
这时候可能就有疑惑了,这个setChildren是哪来的,我们上面说过前端是拿children对象接的,所以我们实体类中怎么可以没有这个对象呢,所以我们自然需要在实体类中把这个对象加进去,一定会报错的,因为这个对象本身并不属于这个表所以需要使用mp的注解
@TableField(exist = false)
private List<Destination> children = new ArrayList<>();
这样一个热门目的地的需求部分就成功做好了,当然这些服务的创建和之前是一样的,参考之前的博客就可以,写了这么久我也大体能总结出一些规律
1.创建模块作为服务,切记不能写错位置
2.导入依赖
3.导入生成的基础代码
4.完成配置(.yml)并写配置类(不要忘记注解)
5.尝试启动,根据报错进行排错,如果能够成功启动说明这个服务已经成功启动了
第二部分(完成单个地点的吐司条显示)
先来看看图
![image]()
这样一条的内容就是吐司条,从这个图中可以看到目的地和**旅游攻略是固定的并不需要我们从后端去拿,中间的三个信息是需要我们去得到的,依旧F12起手,去看看接口
![image]()
非常明显的一个toasts(土司)的英文单词,传了一个destId(目的地id),这里我们采用两种方式去完成这个逻辑
第一种方式 递归
啥是递归,这个东西我个人觉得跟循环也就是哥俩的关系,循环是什么,你有一段代码去循环执行,但是递归也是一段代码去执行,区别是啥,递归循环的是自己的方法,循环可不是,说到这里是不是对这个吐司条的写法有些眉目了
这个吐司条本质是什么呢,我现在有一个destId我能拿到当前二级目录那个条目,我想找上一级,就查询parentId就能拿到上一级的destId,一级一级往上找,直到destId为空说明什么,找到头了,在每次执行都把条目加进去遍历拿信息,这样吐司条就有了,但是注意啊,如果把添加的方法写在重新使用该递归方法的前面,我们拿到的list实际是倒序的,先是二级地址然后一级然后是region,我们需要使用Collctions.reverse去倒序输出才对,若是不想倒序,直接把添加的方法写在重新使用该递归方法后面就好了,只要没有递归到最深处就不添加,这样就可以先拿到region依次是一级目录,二级目录就是正序了
第二种方式头插法
头插法的本质是将双向链表LinkedList的头节点指向新插入的节点,这样就可以不采用递归而是选择while进行destId的非空判断,每次都是用addFirst的专属Api即可
// LinkedList 专门适合头插,addFirst 是 O(1)
LinkedList<Destination> destinationLinkedList = new LinkedList<>();
// 循环向上追溯父级,直到父id等于null停止
while (destId != null) {
// 根据id查询目的地对象
Destination nowDest = baseMapper.selectById(destId);
// 核心:头插,每次新查到的节点放到链表最前边
destinationLinkedList.addFirst(nowDest);
// 把当前id替换成父id,往上一层
destId = nowDest.getParentId();
}
return destinationLinkedList;
单论性能和栈溢出问题我个人更推荐头插法,比递归好。
第三部分(完成单个地点的概况显示)
![image]()
从图中可以看到这是一个关于攻略概况的显示框,这里就需要我们新建一个攻略服务(新建的步骤略,步骤详情参考上文总结的四步)
建好之后,依旧先找接口
![image]()
可以看到是strategies目录下的catalogList,这里我需要去前端看看需要什么数据
<div v-show="overviewState" class="overview-drop" data-cs-p="概况菜单" style="max-height: 520px; overflow: auto" @mouseleave="hideOverview()">
<div class="drop-bd">
<dl v-for="item in catalogsList" :key="item.id" class="clearfix">
<dt>
<a :href="'/destination/survey/' + destId" target="_blank">{{ item.name }}</a>
</dt>
<dd>
<a v-for="(item2, index) in item.strategies" :key="item2.id" :href="'/strategy/article/' + item2.id" target="_blank"> {{ item2.title }}<span v-if="index < item.strategies.length">|</span> </a>
</dd>
</dl>
</div>
可以看到需要的是id和name和title,去实体类看了一圈就攻略分类这个类最符合要求,但是还是缺少一个title字段,这个字段在攻略的实体类里,但是本身表里并没有这个字段所以不要忘了加上注解
@TableField(exist = false)
private List<Strategy> strategies = new ArrayList<>();
这样就不需要我们再建一个Vo去提交数据了,这样的话,controller的返回值也确定了,那就好写了
@GetMapping("/catalogList")
public R<List<StrategyCatalog>> catalogList(Long destId) {
return R.ok(strategyService.queryCatalogList(destId));
}
然后去实现queryCatalogList,最简单粗暴的方式我们需要两个实体类中数据的组合,那就直接给他俩全查了,前端你自己去挑选想要的数据
LambdaQueryWrapper<StrategyCatalog> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(StrategyCatalog::getDestId, destId);
List<StrategyCatalog> strategyCatalogList = strategyCatalogMapper.selectList(queryWrapper);
for(StrategyCatalog catalog : strategyCatalogList){
Long id = catalog.getId();
List<Strategy> strategyList = lambdaQuery().eq(Strategy::getCatalogId, id).list();
catalog.setStrategies(strategyList);
}
return strategyCatalogList;
由于我们是在stategy的service中写的实现类,所以,startegycatalog无法使用这个类的lambdaQuery条件构造器,所以需要去new一个条件构造器,剩下的我们要做的就是根据destId先查攻略分类,再用分类id去查攻略,攻略肯定是多条的,查一条添加一条就ok,这就是基本逻辑
第四部分(完成单个地点的热门攻略显示)
该功能需要把攻略浏览数前三的数据显示出来,那数据库必然是有一个浏览数字段的,使用orderByDesc直接降序,再使用last拼接一个limit就完事儿,咱们依旧是先来看接口
![image]()
在straties中的viewnnumTop3方法,返回值是攻略strategy的list,这就ok了,所有需要知道的全都知道了,直接开搓
controller
@GetMapping("/viewnnumTop3")
public R<List<Strategy>> viewnnumTop3(Long destId) {
return R.ok(strategyService.viewnnumTop3(destId));
}
实现类
@Override
public List<Strategy> viewnnumTop3(Long destId) {
List<Strategy> list = lambdaQuery().eq(Strategy::getDestId, destId)
.orderByDesc(Strategy::getViewnum)
.last("limit 3")
.list();
return list;
}