返回数据思路
先去vo里面查看,有哪些字段,得出哪些数据可以直接返回,哪些是需要去别的数据库去查
以分页查看回答或评论为例,在这个vo中后面这几项都是需要单独去调用才可以查询到的
提出问题页面
回答问题及评论实现效果
也没渲染只分两层:
对问题的一级回复,称为回答
对回答的回复、对回复的回复,作为第二级,称为评论
提出问题(根据多个原型图得出ER图推断出数据库字典)
原型图
根据上图得出字段:课程id,章节id, 问题标题,问题描述,是否匿名
隐藏字段:提出用户id
最新回答的id(这个时候需要另一个表,根据提出的问题进行回答的表),分页查询最新回答比较慢,效率低,所以在表中添加会好一些
回答数量,用户端是否显示,管理员是否查看
得出ER图
回答、评论
回答和评论的属性基本一致,差别就是:
回答的对象是问题
评论的对象是其它回答或评论
字段:回答内容 是否匿名,对哪一个问题进行回答的id(对哪一个评论进行回答的id), 点赞数
隐藏的字段:回答人的id
代码开发
新增问题
修改问题
分页查询问题
@Override
public PageDTO<QuestionVO> queryQuestionPage(QuestionPageQuery query) {
// 1.参数校验,课程id 不能为空
if (query.getCourseId() == null) {
throw new BadRequestException("课程id不能为空");
}
// 2.获取当前登录用户id
Long userid = UserContext.getUser();
// 3. 分页查询提出的问题 课程号 olnyMine为true才会加userid 没有隐藏
Page<InteractionQuestion> page = this.lambdaQuery()
.select(InteractionQuestion.class, tableFieldInfo -> !tableFieldInfo.getProperty().equals("description"))
.eq(InteractionQuestion::getCourseId, query.getCourseId())
.eq(query.getOnlyMine(), InteractionQuestion::getUserId, userid)
.eq(query.getSectionId() != null, InteractionQuestion::getSectionId, query.getSectionId())
.eq(InteractionQuestion::getHidden, false)
.page(query.toMpPageDefaultSortByCreateTimeDesc());
List<InteractionQuestion> records = page.getRecords();
if (CollUtils.isEmpty(records)) {
return PageDTO.empty(page);
}
// 每一个提出的问题的 最新回答id集合
Set<Long> latestAnswerIdsSet = records.stream().filter(c -> c.getLatestAnswerId() != null)
.map(InteractionQuestion::getLatestAnswerId).collect(Collectors.toSet());
// 5.1 获取没有匿名的用户信息,包括回复的不宁名用户
Set<Long> userSet = records.stream().filter(c -> c.getAnonymity() != true).map(InteractionQuestion::getUserId).collect(Collectors.toSet());
// 4.根据id查询提问者id和最近一次回答的信息
Map<Long, InteractionReply> replyMap = new HashMap<>();
if (CollUtils.isNotEmpty(latestAnswerIdsSet)) { // 可能每个问题都没有最新的回答
List<InteractionReply> replies = replyService.listByIds(latestAnswerIdsSet);
for (InteractionReply reply : replies) {
if (!reply.getAnonymity()) {
userSet.add(reply.getUserId());
}
replyMap.put(reply.getId(), reply);
}
// replyMap = replies.stream().collect(Collectors.toMap(InteractionReply::getId, c -> c));
}
// 5. 远程调用用户服务 获取用户信息 批量
List<UserDTO> userDTOS = userClient.queryUserByIds(userSet);
Map<Long, UserDTO> userDTOMap = userDTOS.stream().collect(Collectors.toMap(UserDTO::getId, c -> c));
// 6.封装vo返回
ArrayList<QuestionVO> voList = new ArrayList<>();
for (InteractionQuestion record : records) {
QuestionVO vo = BeanUtils.copyBean(record, QuestionVO.class);
if (!vo.getAnonymity()) {
UserDTO userDTO = userDTOMap.get(vo.getUserId());
if (userDTO != null) {
vo.setUserName(userDTO.getName());
vo.setUserIcon(userDTO.getIcon());
}
}
InteractionReply interactionReply = replyMap.get(record.getLatestAnswerId());
if (interactionReply != null) {
if (!interactionReply.getAnonymity()) {// 不是匿名设置名称
UserDTO userDTO1 = userDTOMap.get(interactionReply.getUserId());
if (userDTO1 != null) {
vo.setLatestReplyUser(userDTO1.getName());
}
}
vo.setLatestReplyContent(interactionReply.getContent());
}
voList.add(vo);
}
return PageDTO.of(page, voList);
}
管理端分页查询问题
在管理端后台存在问答管理列表页,与用户端类似都是分页查询,但是请求参数和返回值有较大差别:
课程分类数据查询
课程分类字段
course字段里面的分类
分类字段里面有parent_id,第一级菜单父id为0
本地缓存由于无需网络查询,速度非常快。不过由于上述缺点,本地缓存往往适用于数据量小、更新不频繁的数据。而课程分类恰好符合。
Caffeine
适用于数据量少,长时间不会发生变化的
对于常见缓存类型而言,可以分为本地缓存以及分布式缓存两种,Caffeine就是一种优秀的本地缓存,而Redis可以用来做分布式缓存
caffeine是一个命中率搞得本地缓存
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<!--https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeinez 找最新版-->
<version>3.0.5</version>
</dependency>
@Test
void testBasicOps() {
// 构建cache对象
Cache<String, String> cache = Caffeine.newBuilder().build();
// 存数据
cache.put("gf", "迪丽热巴");
// 取数据
String gf = cache.getIfPresent("gf");
System.out.println("gf = " + gf);
// 取数据,包含两个参数:
// 参数一:缓存的key
// 参数二:Lambda表达式,表达式参数就是缓存的key,方法体是查询数据库的逻辑
// 优先根据key查询JVM缓存,如果未命中,则执行参数二的Lambda表达式
String defaultGF = cache.get("defaultGF", key -> {
// 根据key去数据库查询数据
return "柳岩";
});
System.out.println("defaultGF = " + defaultGF);
}
分类的缓存:
private final Cache<String, Map<Long, CategoryBasicDTO>> categoryCaches;
private final CategoryClient categoryClient;
public Map<Long, CategoryBasicDTO> getCategoryMap() {
return categoryCaches.get("CATEGORY", key -> {
// 这里面必须要有return语句
// 1.从CategoryClient查询
List<CategoryBasicDTO> list = categoryClient.getAllOfOneLevel();
if (list == null || list.isEmpty()) {
return CollUtils.emptyMap();
}
// 2.转换数据
return list.stream().collect(Collectors.toMap(CategoryBasicDTO::getId, Function.identity()));
});
}
public String getCategoryNames(List<Long> ids) {
if (ids == null || ids.size() == 0) {
return "";
}
// 1.读取分类缓存
Map<Long, CategoryBasicDTO> map = getCategoryMap();
// 2.根据id查询分类名称并组装
StringBuilder sb = new StringBuilder();
for (Long id : ids) {
sb.append(map.get(id).getName()).append("/");
}
// 3.返回结果
return sb.deleteCharAt(sb.length() - 1).toString();
}
其中CategoryCacheConfig
是Caffeine的缓存配置:
public class CategoryCacheConfig {
/**
* 课程分类的caffeine缓存
*/
@Bean
public Cache<String, Map<Long, CategoryBasicDTO>> categoryCaches(){
return Caffeine.newBuilder()
.initialCapacity(1) // 容量限制
.maximumSize(10_000) // 最大内存限制
.expireAfterWrite(Duration.ofMinutes(30)) // 有效期
.build();
}
/**
* 课程分类的缓存工具类
*/
@Bean
public CategoryCache categoryCache(
Cache<String, Map<Long, CategoryBasicDTO>> categoryCaches, CategoryClient categoryClient){
return new CategoryCache(categoryCaches, categoryClient);
}
}
新增回答或评论
基本属性:内容,是否匿名、对哪一个问题做的回答,当前用户id
如果是针对某个回答发表的评论,则有新的关联属性:
回答id:评论是在哪个回答下面的
目标评论id:当前评论是针对哪一条评论的
目标用户id:当前评论是针对哪一个用户的
评论或回答表结构
如果是回答:answer_id 为空,question_id就是对哪个问题做的回答
如果评论:回复的目标回复id就是另一条数据的主键id
分页查询回答
管理员端查看回答