目录
一、热点问题列表---持久层
查询热点问题列表时,需要执行的SQL语句大致是:
select id, title, status, hits from question where is_delete=0 order by hits desc, gmt_create desc limit 0, 10
先在straw-commons
的cn.tedu.straw.commons.vo
包中创建QuestionMostHitsVO
类,用于封装以上查询结果:
package cn.tedu.straw.commons.vo;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data
@Accessors(chain = true)
public class QuestionMostHitsVO implements Serializable {
private Integer id;
private String title;
private Integer status;
private Integer hits;
}
在straw-api-question
的cn.tedu.straw.api.question.mapper.QuestionMapper
接口中添加抽象方法:
@Repository
public interface QuestionMapper extends BaseMapper<Question> {
/**
* 查询热点问题列表
*
* @return 热点问题列表
*/
List<QuestionMostHitsVO> findHostHitsList();
}
并在QuestionMapper.xml
中配置以上抽象方法映射的SQL语句:
<select id="findHostHitsList"
resultType="cn.tedu.straw.commons.vo.QuestionMostHitsVO">
SELECT
id, title, status, hits
FROM
question
WHERE
is_delete=0
ORDER BY
hits DESC, gmt_create DESC
LIMIT 0, 10
</select>
完成后,在QuestionMapperTests
中编写并执行单元测试:
@SpringBootTest
@Slf4j
public class QuestionMapperTests {
@Autowired
QuestionMapper mapper;
@Test
void findHostHitsList() {
List<QuestionMostHitsVO> questions = mapper.findHostHitsList();
log.debug("热点问题数量:{}", questions.size());
for (QuestionMostHitsVO question : questions) {
log.debug(">>> {}", question);
}
}
}
二、热点问题列表---业务层
在IQuestionService
接口中添加抽象方法:
/**
* 查询热点问题列表
*
* @return 热点问题列表
*/
List<QuestionMostHitsVO> getHostHitsQuestions();
并在QuestionServiceImpl
类中实现以上抽象方法:
@Override
public List<QuestionMostHitsVO> getHostHitsQuestions() {
return questionMapper.findHostHitsList();
}
完成后,在QuestionServiceTests
中编写并执行单元测试:
@Test
void getHostHitsQuestions() {
List<QuestionMostHitsVO> questions = service.getHostHitsQuestions();
log.debug("热点问题数量:{}", questions.size());
for (QuestionMostHitsVO question : questions) {
log.debug(">>> {}", question);
}
}
以上数据也可以通过定时数据缓存到Redis中,具体做法可参考“问题标签列表”的缓存处理。 查看详情
三、热点问题列表---控制器层
在QuestionController
中添加处理请求的方法:
// http://localhost:8081/v1/questions/most-hits
// http://localhost/api-question/v1/questions/most-hits
@GetMapping("/most-hits")
public R<List<QuestionMostHitsVO>> getHostHitsQuestions() {
// 调用业务对象查询数据
// 返回成功,及查询到的数据
return R.ok(questionService.getHostHitsQuestions());
}
完成后,在浏览器中通过 http://localhost:8081/v1/questions/most-hits 测试访问,测试通过后,再通过 http://localhost/api-question/v1/questions/most-hits 也测试访问。
四、热点问题列表---前端页面
1.在主页通过模拟数据显示“热点问题列表”
由于“主页”和“发布问题”等多个页面都会显示“热点问题列表”,应该将“显示热点问题列表”的功能在“主页”实现,然后,在其它页面中复用即可!
先在index.html
中找到“热点问题”列表的显示区域,为该区域的父级标签添加id
属性,用于对应Vue对象:
在static/js/commons/most_hits.js
文件,创建Vue对象:
在index.html
中引用以上JS文件:
在most_hits.js
中声明模拟数据的Vue属性,并添加模拟数据:
在index.html
中配置Vue标签以显示模拟数据,首先,收起<div id="mostHitsApp">
的各子级<div>
标签,这些子级<div>
标签就是原始页面中显示的一个个“问题”:
由于这些标签即将通过JS代码循环生成,所以,删除这里的若干个<div>
标签,只保留1个即可:
剩余的<div>
标签是整个的被遍历的标签,展开这个<div>
标签,先添加v-for
进行遍历:
然后,设置显示“标题”:
同理,显示“点击量” :
关于“状态”的显示,原本的代码是:
以上代码中,使用了3个<small>
标签显示不同的“状态”,只不过前2个都被设置了style="display: none"
所以并没有显示出来!
可以看到,以上3个<small>
标签的代码是高度相似的,可以只使用1个<small>
标签,将class
的值和<small>
标签内部显示的文字都设置为变量即可!
则在Vue的模拟数据中补充2个属性:
在HTML中,删除以上3个<small>
标签中的2个,仅保留1个,并配置:
在使用Vue时,可以在任何页面标签中添加
v-bind
用于绑定该标签的某个属性到Vue中,例如每个标签都有class
属性,则可以通过v-bind:class="VueData"
将该标签的class
属性绑定为VueData
的值,如果VueData
的值是text-info
,则相当于该标签配置了class="text-info"
,也可以为<a>
标签绑定v-bind:href="xxx"
,或为<img>
标签绑定v-bind:src="xxx"
……
完成后,重启straw-gateway
项目,刷新主页,则模拟数据应该可以正常显示。
2.使用真实数据显示“热点问题列表”
3. 在其它页面复用“热点问题列表”
在index.html
中将整个显示“热点问题列表”的区域配置th:fragment
属性,将它设置为一个“碎片”:
在question/create.html
中,将原有显示“热点问题列表”的区域全部删除,自定义标签,使用th:replace
替换为碎片即可:
然后,在question/create.html
中引用most_hits.js
文件:
完成后,重启straw-gateway
项目,在“主页”和“发布问题”页面都可以看到相同的“热点问题列表”。
五、显示“我的问答”列表--持久层
显示“我的问答”需要执行的SQL查询大致是:
select
id, title, content, user_nick_name, status, hits, tag_ids, gmt_create
from question where user_id=? order by gmt_create
在设计以上查询时,不需要考虑
Limit
子句,后续将使用PageHelper
框架来实现分页处理。
由于没有任何类适用封装以上查询结果,应该先在straw-commons
的cn.tedu.straw.commons.vo
包中创建QuestionListItemVO
类:
@Data
@Accessors(chain = true)
public class QuestionListItemVO implements Serializable {
private Integer id;
private String title;
private String content;
private String userNickName;
private Integer status;
private Integer hits;
private String tagIds;
private LocalDateTime gmtCreate;
}
在QuestionMapper.java
接口中添加抽象方法:
/**
* 查询某用户的“问题”列表
*
* @param userId 用户的id
* @return 该用户的“问题”列表
*/
List<QuestionListItemVO> findByUserId(Integer userId);
在QuestionMapper.xml
中配置以上抽象方法映射的SQL语句:
<select id="findByUserId" resultType="cn.tedu.straw.commons.vo.QuestionListItemVO">
SELECT
id,
title,
content,
user_nick_name AS userNickName,
status,
hits,
tag_ids AS tagIds,
gmt_create AS gmtCreate
FROM
question
WHERE
user_id=#{userId}
ORDER BY
gmt_create DESC
</select>
完成后,在QuestionMapperTests
中编写并执行单元测试:
@Test
void findByUserId() {
Integer userId = 10;
List<QuestionListItemVO> questions = mapper.findByUserId(userId);
log.debug("某用户的id:{},该用户的问题数量:{}", userId, questions.size());
for (QuestionListItemVO question : questions) {
log.debug(">>> {}", question);
}
}
六、关于PageHelper框架
PageHelper是一款无侵入的分页框架,其依赖的参考代码是:
<!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper-spring-boot-starter -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.13</version>
</dependency>
当添加了依赖后,在执行持久层的查询之前,调用PageHelper.startPage()
方法设置分页参数,即可实现分页,原有的持久层代码不需要做任何修改:
另外,PageHelper框架还提供了PageInfo
类,该类可以用于封装查询得到的结果,并提供一系列的分页相关参数,例如:
以上代码中,PageInfo
中的泛型就是查询时List
集合中的泛型,在创建PageInfo
对象时,将查询得到的对象作为构造方法参数传入即可,得到的PageInfo
对象输出结果例如:
PageInfo{
pageNum=2,
pageSize=3,
size=3,
startRow=4,
endRow=6,
total=14,
pages=5,
list=Page{
count=true,
pageNum=2,
pageSize=3,
startRow=3,
endRow=6,
total=14,
pages=5,
reasonable=false,
pageSizeZero=false
}
[....],
prePage=1,
nextPage=3,
isFirstPage=false,
isLastPage=false,
hasPreviousPage=true,
hasNextPage=true,
navigatePages=8,
navigateFirstPage=1,
navigateLastPage=5,
navigatepageNums=[1, 2, 3, 4, 5]
}
关于PageInfo
的一些关键属性,当前共14条数据,每页显示3条,当查询页码为1
时,关键属性为:
prePage=0,
nextPage=2,
isFirstPage=true,
isLastPage=false,
hasPreviousPage=false,
hasNextPage=true,
navigatePages=8,
navigateFirstPage=1,
navigateLastPage=5,
navigatepageNums=[1, 2, 3, 4, 5]
当查询页码为0
时,其实会查出第1页的数据,且关键属性为:
prePage=0,
nextPage=1,
isFirstPage=false,
isLastPage=false,
hasPreviousPage=false,
hasNextPage=true,
navigatePages=8,
navigateFirstPage=1,
navigateLastPage=5,
navigatepageNums=[1, 2, 3, 4, 5]
当查询页码为-1
时,其实会查出第1页的数据,且关键属性为:
prePage=0,
nextPage=0,
isFirstPage=false,
isLastPage=false,
hasPreviousPage=false,
hasNextPage=true,
navigatePages=8,
navigateFirstPage=1,
navigateLastPage=5,
navigatepageNums=[1, 2, 3, 4, 5]
当查询页码为5
时(当前最大页码),将查询第5页数据,关键属性为:
prePage=4,
nextPage=0,
isFirstPage=false,
isLastPage=true,
hasPreviousPage=true,
hasNextPage=false,
navigatePages=8,
navigateFirstPage=1,
navigateLastPage=5, navigatepageNums=[1, 2, 3, 4, 5]
当查询页码为6
时(超出当前最大页码),将不执行查询数据,关键属性为:
prePage=5,
nextPage=0,
isFirstPage=false,
isLastPage=false,
hasPreviousPage=true,
hasNextPage=false,
navigatePages=8,
navigateFirstPage=1,
navigateLastPage=5, navigatepageNums=[1, 2, 3, 4, 5]
七、显示“我的问答”列表--业务层
在IQuestionService
接口中添加抽象方法:
/**
* 获取当前登录的用户的“问答”列表
*
* @param pageNum 页码
* @param userId 用户的id
* @return 该用户的“问答”列表
*/
PageInfo<QuestionListItemVO> getMyQuestions(Integer pageNum, Integer userId);
在application.properties
中将“每页显示几条问题”添加为自定义配置:
# 自定义配置:在“我的问答”列表中,每页显示几条数据
project.question.my.page-size=3
在QuestionServiceImpl
类中实现以上方法:
@Value("${project.question.my.page-size}")
Integer pageSize;
@Override
public PageInfo<QuestionListItemVO> getMyQuestions(Integer pageNum, Integer userId) {
if (pageNum == null || pageNum < 1) {
pageNum = 1;
}
PageHelper.startPage(pageNum, pageSize);
List<QuestionListItemVO> questions = questionMapper.findByUserId(userId);
return new PageInfo<>(questions);
}
完成后,在QuestionServiceTests
中编写并执行单元测试:
@Test
void getMyQuestions() {
Integer pageNum = 1;
Integer userId = 10;
PageInfo pageInfo = service.getMyQuestions(pageNum, userId);
log.debug("PageInfo >>> {}", pageInfo);
}
八、显示“我的问答”列表--控制器层
在QuestionController
中添加处理请求的方法:
// http://localhost/api-question/v1/questions/my?page=1
@GetMapping("/my")
public R<PageInfo<QuestionListItemVO>> getMyQuestions(Integer page,
@AuthenticationPrincipal LoginUserInfo loginUserInfo) {
if (page == null || page < 1) {
page = 1;
}
Integer userId = loginUserInfo.getId();
PageInfo<QuestionListItemVO> pageInfo = questionService.getMyQuestions(page, userId);
return R.ok(pageInfo);
}
完成后,重启项目,通过以上注释部分的网关的URL进行测试访问。
附:关于GET与POST
使用GET方式的请求会将请求参数暴露在URL中,例如:http://localhost:8080/users/reg?username=root&password=1234,所以,GET方式的请求不适用于提交敏感数据(例如涉及账号安全的、涉及账号隐私的等等),同时,由于URL的长度是有限的,也不适用于提交较大数据(例如一篇文章、上传文件等)。
关于URL的长度限制,需要同时参考客户端的浏览器限制,和服务器端的限制,通常建议将该限制值视为2K。
使用POST方式的请求会将请求参数进行封装再提交(没有特殊的加密处理),请求参数不会暴露在URL中,相对更加安全,也不受长度限制。
尽管GET方式的缺点多,而POST方式没有这些缺点,但是,GET方式的请求也是不可以被POST完全取代的,在某些场景中,是需要将请求参数暴露在URL中的!例如在实现“分享”、“收藏”等功能时就需要使用GET方式的请求!
另外,GET方式的请求的执行效率是高于POST方式的请求的!