项目之提问页面-显示问题、发表问题(8)
🌊 作者主页:海拥
🌊 简介:🏆CSDN全栈领域优质创作者、🥇HDZ核心组成员、🥈蝉联C站周榜前十
🌊 粉丝福利:粉丝群 每周送六本书,不定期送各种小礼品
30. 提问页面-显示问题标签的下拉列表
关于js代码:
Vue.component('v-select', VueSelect.VueSelect);
let createQuestionApp = new Vue({
el: '#createQuestionApp',
data: {
tags: [
{label: 'MyBatis Plus', value: 1},
{label: 'Spring Security', value: 2},
{label: 'Spring Validation', value: 3},
{label: 'Lombok', value: 4},
{label: 'Vue', value: 5}
],
selectedTags: []
},
methods: {
loadTags: function () {
$.ajax({
url: '/api/v1/tags',
type: 'get',
success: function(json) {
let tags = [];
for (let i = 0; i < json.data.length; i++) {
let op = {
label: json.data[i].name,
value: json.data[i].id
};
tags[i] = op;
}
createQuestionApp.tags = tags;
}
});
}
},
created: function () {
this.loadTags();
}
});
31. 提问页面-显示老师的下拉列表
查询老师列表的SQL语句:
select id, nickname, gender, phone from user where type=1 order by id;
先创建TeacherVO
类:
@Data
public class TeacherVO {
private Integer id;
private String nickname;
private Integer gender;
private String phone;
}
在UserMapper
接口中添加:
/**
* 查询老师的列表
*
* @return 老师的列表
*/
List<TeacherVO> findTeachers();
在UserMapper.xml
中配置映射:
<select id="findTeachers" resultType="cn.tedu.straw.portal.vo.TeacherVO">
SELECT
id, nickname, phone, gender
FROM
user
WHERE
type=1 AND enabled=1 AND locked=0
ORDER BY
id
</select>
在UserMapperTests
中测试:
@Test
void findTeachers() {
List<TeacherVO> teachers = userMapper.findTeachers();
log.debug("teacher count = {}", teachers.size());
for (TeacherVO teacher : teachers) {
log.debug(">>> {}", teacher);
}
}
在IUserService
中添加:
/**
* 获取缓存的老师的列表,如果列表为空,还会尝试从数据库查询列表数据,避免因为缓存为空导致无法获取到数据
*
* @return 缓存的老师的列表
*/
List<TeacherVO> findTeachers();
/**
* 获取缓存的老师的列表,由于存在清空缓存机制,获取到的数据将不可靠
*
* @return 缓存的老师的列表
*/
List<TeacherVO> findCachedTeachers();
在UserServiceImpl
中实现以上2个方法:
/**
* 缓存的老师列表
*/
private List<TeacherVO> teachers = new CopyOnWriteArrayList<>();
@Override
public List<TeacherVO> findTeachers() {
if (teachers.isEmpty()) {
synchronized (CacheSchedule.LOCK_CACHE) {
if (teachers.isEmpty()) {
teachers.addAll(userMapper.findTeachers());
}
}
}
return teachers;
}
@Override
public List<TeacherVO> findCachedTeachers() {
return teachers;
}
在CacheSchedule
的计划任务中,清除Tag
数据缓存时,一并清除Teacher
数据缓存:
@Component
@EnableScheduling
@Slf4j
public class CacheSchedule {
@Autowired
private ITagService tagService;
@Autowired
private IUserService userService;
/**
* <p>缓存锁,凡是写入(添加、移除)缓存的数据时使用这个锁</p>
* <p>public:多个类都需要使用到这把锁</p>
* <p>static:具有唯一的特性,能保证实现互斥</p>
* <p>final:不允许任何位置修改或重新创建对象</p>
* <p>Object:不关心锁的类型,只要是对象,都可以当作锁来用</p>
*/
public static final Object LOCK_CACHE = new Object();
@Scheduled(initialDelay = 10 * 60 * 1000, fixedRate = 10 * 60 * 1000)
public void clearCache() {
synchronized (LOCK_CACHE) {
tagService.getCachedTags().clear();
log.debug("clear tags cache ...");
userService.findCachedTeachers().clear();
log.debug("clear teacher cache ...");
}
}
}
注意:需要修改原TagServiceImpl
中处理缓存数据时使用的锁对象。
在UserServiceTests
中测试:
@Test
void findTeachers() {
List<TeacherVO> teachers = userService.findTeachers();
log.debug("teacher count = {}", teachers.size());
for (TeacherVO teacher : teachers) {
log.debug(">>> {}", teacher);
}
}
关于控制器层:
// http://localhost:8080/api/v1/users/teacher/list
@GetMapping("/teacher/list")
public R<List<TeacherVO>> getTeachers() {
return R.ok(userService.findTeachers());
}
为了使得为null
的数据不会出现在服务器端响应的JSON结果中,可以在application.properties中添加配置:
spring.jackson.default-property-inclusion=non_null
在前端页面中,关于显示“老师”的下拉列表,先将原有的<select>
替换为:
<v-select :options="teachers" v-model="selectedTeacherIds"
:reduce="option => option.value"
:selectable="() => selectedTeacherIds.length < 3"
multiple required
placeholder="请选择回答问题的老师(最多可以选择3个)">
</v-select>
然后,在create.js中,在Vue对象的data
中添加teachers
和selectedTeacherIds
这2个属性:
data: {
tags: [],
selectedTagIds: [],
teachers: [],
selectedTeacherIds: []
}
在methods
中补充添加新的方法,用于加载数据并填充下拉列表:
loadTeacher: function () {
$.ajax({
url: '/api/v1/users/teacher/list',
type: 'get',
success: function (json) {
let teachers = [];
for (let i = 0; i < json.data.length; i++) {
let teacher = {
label: json.data[i].nickname,
value: json.data[i].id
}
teachers[i] = teacher;
}
createQuestionApp.teachers = teachers;
}
});
}
最后,在created
中补充调用以上方法:
this.loadTeacher();
完整的js代码:
Vue.component('v-select', VueSelect.VueSelect);
let createQuestionApp = new Vue({
el: '#createQuestionApp',
data: {
tags: [],
selectedTagIds: [],
teachers: [],
selectedTeacherIds: []
},
methods: {
loadTags: function () {
$.ajax({
url: '/api/v1/tags',
type: 'get',
success: function (json) {
let tags = [];
for (let i = 0; i < json.data.length; i++) {
let op = {
label: json.data[i].name,
value: json.data[i].id
};
tags[i] = op;
}
createQuestionApp.tags = tags;
}
});
},
loadTeacher: function () {
$.ajax({
url: '/api/v1/users/teacher/list',
type: 'get',
success: function (json) {
let teachers = [];
for (let i = 0; i < json.data.length; i++) {
let teacher = {
label: json.data[i].nickname,
value: json.data[i].id
}
teachers[i] = teacher;
}
createQuestionApp.teachers = teachers;
}
});
}
},
created: function () {
this.loadTags();
this.loadTeacher();
}
});
32. 发表问题-持久层
导入question
及相关数据表(共3张表),通过代码生成器项目生成基础代码文件。
打开model
包中新生成的实体类,在各实体类之前都添加注解:
@Accessors(chain = true)
则后续创建实体类对象就,就可以使用链式语法更快捷的为属性赋值!
本次“发表问题”时,持久层主要处理的就是“向各数据表中插入数据”,插入数据时,各数据应该都是完整的(将由业务层补全数据),由MyBatis Plus自带的insert()
方法足以满足插入数据的需求!
另外,一般情况下,在向任何数据表中插入/删除/修改数据之前,都需要考虑“是否需要通过查询,提前进行相关检查”,考虑的问题大多是“允许插入的数据的数量是否达到上限”、“某些字段的值是否允许重复”、“相关数据是否存在”、“是否具有访问这些数据的权限”……本次需要实现的“发表问题”功能暂时没有需要检查的项。
33. 发表问题-业务层
首先,需要创建一个DTO类,表示用于封装客户端将向服务器端提交的数据的类型!所以,应该先创建一个类,类中的属性与客户端将要提交的数据保持一致即可!则在cn.tedu.straw.portal
包中创建dto
子包,并在这个包中创建QuestionDTO
类:
package cn.tedu.straw.portal.dto;
import lombok.Data;
@Data
public class QuestionDTO {
private String title;
private Integer[] tagIds;
private Integer[] teacherIds;
private String content;
}
然后,在业务接口IQuestionService
中添加抽象方法:
/**
* 发布问题
*
* @param questionDTO 从客户端提交过来的数据
* @param uid 当前登录的用户id
* @param userNickname 用户登录的用户昵称
*/
void create(QuestionDTO questionDTO, Integer uid, String userNickname);
在业务实现类QuestionServiceImpl
中规划该抽象方法的实现步骤:
@Autowired
private QuestionMapper questionMapper;
@Autowired
private QuestionTagMapper questionTagMapper;
@Autowired
private UserQuestionMapper userQuestionMapper;
@Transactional
public void create(QuestionDTO questionDTO, Integer uid, String userNickname) {
// 创建当前时间对象:now
// 将questionDTO中的tagIds转换成例如 2,7,9 这种格式的字符串,名为tagIdsStr
// 创建Question对象
// 向Question对象中补全数据
// - title / content > questionDTO的title / content
// - userId / userNickName > 参数
// - status > 0
// - hits > 0
// - isPublic > 1
// - createdTime / modifiedTime > now
// - isDelete > 0
// - tagIds > tagIdsStr
// 基于以上Question对象,调用questionMapper的insert()方法,向question表中插入数据,获取返回值
// 判断返回值是否不为1
// 是:抛出InsertException
// 遍历questionDTO中的tagIds
// - 创建QuestionTag对象
// - 补全属性:questionId > 以上插入Question对象的id
// - 补全属性:tagId > 被遍历到的数据
// - 基于以上QuestionTag对象,调用questionTagMapper的insert()方法,向question_tag表中插入数据,以记录“问题”与“标签”的对应关系,并需要获取当前调用方法的返回值
// - 判断返回值是否不为1
// - 是:抛出InsertException
// 遍历questionDTO中的teacherIds
// - 创建UserQuestion对象
// - 补全属性:questionId > 以上插入Question对象的id
// - 补全属性:userId > 被遍历到的数据
// - 补全属性:createdTime > now
// - 基于以上UserQuestion对象,调用userQuestionMapper的insert()方法,向user_question表中插入数据,以记录“问题”与“回答问题的老师”的对应关系,并需要获取当前调用方法的返回值
// - 判断返回值是否不为1
// - 是:抛出InsertException
}
- 当开发某功能时,如果没有思路,直接编写最后一步,然后,需要什么数据,就在之前补全什么数据。
- 当缺少某个数据时,这个数据要么直接声明为方法的参数,最终将由方法的调用者来决定数据的值,或者,自行编写相关代码得到这个数据的值。
- 如果创建了对象,需要检查对象的各属性值,如果某些属性是应该由客户端提交的,可以基于参数赋值或不处理,另一些属性不是由客户端提交的数据,必须补全这些属性的值!
实现业务方法:
@Service
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements IQuestionService {
@Autowired
private QuestionMapper questionMapper;
@Autowired
private QuestionTagMapper questionTagMapper;
@Autowired
private UserQuestionMapper userQuestionMapper;
@Override
@Transactional
public void create(QuestionDTO questionDTO, Integer userid, String userNickname) {
// 创建当前时间对象:now
LocalDateTime now = LocalDateTime.now();
// 将questionDTO中的tagIds转换成例如 2,7,9 这种格式的字符串,名为tagIdsStr
String tagIdsStr = Arrays.toString(questionDTO.getTagIds()); // [2, 7, 9]
tagIdsStr = tagIdsStr.substring(1, tagIdsStr.length() - 1); // 2, 7, 9
// 创建Question对象
// 向Question对象中补全数据
// - title / content > questionDTO的title / content
// - userId / userNickName > 参数
// - status > 0
// - hits > 0
// - isPublic > 1
// - createdTime / modifiedTime > now
// - isDelete > 0
// - tagIds > tagIdsStr
Question question = new Question()
.setTitle(questionDTO.getTitle())
.setContent(questionDTO.getContent())
.setUserId(userid)
.setUserNickName(userNickname)
.setStatus(0).setHits(0).setIsPublic(1).setIsDelete(0)
.setCreatedTime(now).setModifiedTime(now)
.setTagIds(tagIdsStr);
// 基于以上Question对象,调用questionMapper的insert()方法,向question表中插入数据,获取返回值
int rows = questionMapper.insert(question);
// 判断返回值是否不为1
if (rows != 1) {
// 是:抛出InsertException
throw new InsertException("发布问题失败!当前服务器忙,请稍后再尝试!");
}
// 遍历questionDTO中的tagIds
for (Integer tagId : questionDTO.getTagIds()) {
// - 创建QuestionTag对象
// - 补全属性:questionId > 以上插入Question对象的id
// - 补全属性:tagId > 被遍历到的数据
QuestionTag questionTag = new QuestionTag()
.setQuestionId(question.getId())
.setTagId(tagId);
// - 基于以上QuestionTag对象,调用questionTagMapper的insert()方法,向question_tag表中插入数据,以记录“问题”与“标签”的对应关系,并需要获取当前调用方法的返回值
rows = questionTagMapper.insert(questionTag);
// - 判断返回值是否不为1
if (rows != 1) {
// 是:抛出InsertException
throw new InsertException("发布问题失败!当前服务器忙,请稍后再尝试!");
}
}
// 遍历questionDTO中的teacherIds
for (Integer teacherId : questionDTO.getTeacherIds()) {
// - 创建UserQuestion对象
// - 补全属性:questionId > 以上插入Question对象的id
// - 补全属性:userId > 被遍历到的数据
// - 补全属性:createdTime > now
UserQuestion userQuestion = new UserQuestion()
.setQuestionId(question.getId())
.setUserId(teacherId)
.setCreatedTime(now);
// - 基于以上UserQuestion对象,调用userQuestionMapper的insert()方法,向user_question表中插入数据,以记录“问题”与“回答问题的老师”的对应关系,并需要获取当前调用方法的返回值
rows = userQuestionMapper.insert(userQuestion);
// - 判断返回值是否不为1
if (rows != 1) {
// 是:抛出InsertException
throw new InsertException("发布问题失败!当前服务器忙,请稍后再尝试!");
}
}
}
}
在src/test/java的cn.tedu.straw.portal.service
包中创建业务测试类QuestionServiceTests
,编写并执行单元测试:
@SpringBootTest
@Slf4j
public class QuestionServiceTests {
@Autowired
IQuestionService service;
@Test
void create() {
try {
QuestionDTO questionDTO = new QuestionDTO();
questionDTO.setTitle("SpringSecurity验证时记录了用户的ID吗?");
questionDTO.setContent("SpringSecurity自动完成验证,可以获取用户名,但是,用户ID在哪里获取?");
questionDTO.setTagIds(new Integer[] { 5, 8, 13 });
questionDTO.setTeacherIds(new Integer[] { 2, 3 });
Integer userId = 5;
String userNickname = "超级码农";
service.create(questionDTO, userId, userNickname);
log.debug("create question ok.");
} catch (ServiceException e) {
log.debug("create question failure.", e);
}
}
}
34. 发表问题-控制器层
在QuestionController
中添加处理请求的方法,此次处理请求时,路径可以设计为/api/v1/questions/create
,请求类型应该是post
,客户端将需要提交QuestionDTO
类型的参数,另外,还需要通过@AuthenticationPriciple
注入当前登录的用户信息,发表问题成功后,响应R
表示成功即可。
@RestController
@RequestMapping("/api/v1/questions")
public class QuestionController {
@Autowired
private IQuestionService questionService;
// http://localhost:8080/api/v1/questions/create?title=Java&content=HelloWorld&tagIds=3&tagIds=9&tagIds=15&teacherIds=1&teacherIds=3
@RequestMapping("/create")
public R<Void> create(QuestionDTO questionDTO,
@AuthenticationPrincipal UserInfo userInfo) {
questionService.create(questionDTO, userInfo.getId(), userInfo.getNickname());
return R.ok();
}
}
34. 发表问题-补全页面功能
关于提交请求并处理结果的函数:
function () {
let content = $('#summernote').val();
console.log("标题:" + this.title);
console.log("选中的标签:" + this.selectedTagIds);
console.log("选中的老师:" + this.selectedTeacherIds);
console.log("正文:" + content);
$.ajax({
url: '/api/v1/questions/create',
type: 'post',
traditional: true,
data: {
title: createQuestionApp.title,
tagIds: createQuestionApp.selectedTagIds,
teacherIds: createQuestionApp.selectedTeacherIds,
content: content
},
success: function (json) {
if (json.state == 2000) {
alert("发表问题成功!!!")
} else {
alert(json.message);
}
}
});
}
作业:显示热点问题列表
通过界面添加不少于10个问题!
查询热点问题列表的SQL语句大致是(暂时不解决“x条回答”的数据):
select id, title, status, hits
from question
where is_public=1 and is_delete=0
order by hits desc
limit 0, 10
则应该先创建新的VO类进行封装:
@Data
public class QuestionListItemVO {
// 声明id, title, status, hits
}
持久层接口:
List<QuestionListItemVO> findMostHits();
配置SQL语句;
创建QuestionMapperTests
测试类,并测试以上方法。
业务层接口:
List<QuestionListItemVO> getMostHits();
List<QuestionListItemVO> getCachedMostHits();
利用缓存的做法实现以上方法。
另行设计计划任务,因为更新“热点问题”缓存的间隔时间应该与“标签”、“老师”的不同,更新缓存的频率应该更高(间隔时间更短)。
控制器设计请求路径为http://localhost:8080/api/v1/questions/hits,将返回R<List<QuestionListItemVO>>
。
在js/commons文件夹下创建question_most_hits.js文件,结合create.html文件,将列表数据显示出来。
- 点赞
- 收藏
- 关注作者
评论(0)