天机学堂3-ES+Caffeine
文章目录
day05-问答系统
效果:
表
互动提问的问题表:
CREATE TABLE IF NOT EXISTS `interaction_question` (
`id` bigint NOT NULL COMMENT '主键,互动问题的id',
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '互动问题的标题',
`description` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '问题描述信息',
`course_id` bigint NOT NULL COMMENT '所属课程id',
`chapter_id` bigint NOT NULL COMMENT '所属课程章id',
`section_id` bigint NOT NULL COMMENT '所属课程节id',
`user_id` bigint NOT NULL COMMENT '提问学员id',
`latest_answer_id` bigint DEFAULT NULL COMMENT '最新的一个回答的id',
`answer_times` int unsigned NOT NULL DEFAULT '0' COMMENT '问题下的回答数量',
`anonymity` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否匿名,默认false',
`hidden` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否被隐藏,默认false',
`status` tinyint DEFAULT '0' COMMENT '管理端问题状态:0-未查看,1-已查看',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '提问时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_course_id` (`course_id`) USING BTREE,
KEY `section_id` (`section_id`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='互动提问的问题表';
回答或评论表:
CREATE TABLE IF NOT EXISTS `interaction_reply` (
`id` bigint NOT NULL COMMENT '互动问题的回答id',
`question_id` bigint NOT NULL COMMENT '互动问题问题id',
`answer_id` bigint DEFAULT '0' COMMENT '回复的上级回答id',
`user_id` bigint NOT NULL COMMENT '回答者id',
`content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '回答内容',
`target_user_id` bigint DEFAULT '0' COMMENT '回复的目标用户id',
`target_reply_id` bigint DEFAULT '0' COMMENT '回复的目标回复id',
`reply_times` int NOT NULL DEFAULT '0' COMMENT '评论数量',
`liked_times` int NOT NULL DEFAULT '0' COMMENT '点赞数量',
`hidden` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否被隐藏,默认false',
`anonymity` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否匿名,默认false',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_question_id` (`question_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='互动问题的回答或评论';
KEY 关键字用于定义索引,而 USING BTREE 是一个可选的子句,用于显式指定索引的存储类型。如果不指定 USING BTREE,MySQL 会默认使用 B-Tree 索引结构
用户端分页查询问题
目标效果
代码实现
3.6.管理端分页查询问题
ES相关
Feign接口
package com.tianji.api.client.search;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@FeignClient("search-service")
public interface SearchClient {
@GetMapping("/courses/name")
List<Long> queryCoursesIdByName(
@RequestParam(value = "keyword", required = false) String keyword);
}
Controller:
package com.tianji.search.controller;
import com.tianji.common.domain.dto.PageDTO;
import com.tianji.search.domain.query.CoursePageQuery;
import com.tianji.search.domain.vo.CourseVO;
import com.tianji.search.service.ICourseService;
import com.tianji.search.service.ISearchService;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.*;
import springfox.documentation.annotations.ApiIgnore;
@RestController
@RequestMapping("courses")
@Api(tags = "课程搜索接口")
@RequiredArgsConstructor
public class CourseController {
private final ISearchService searchService;
private final ICourseService courseService;
@ApiOperation("用户端课程搜索接口")
@GetMapping("/portal")
public PageDTO<CourseVO> queryCoursesForPortal(CoursePageQuery query){
return searchService.queryCoursesForPortal(query);
}
@ApiIgnore
@GetMapping("/name")
public List<Long> queryCoursesIdByName(@RequestParam("keyword") String keyword){
return searchService.queryCoursesIdByName(keyword);
}
管理端互动问题分页实现
管理端互动问题分页: Admin可以通过关键字搜索课程,但由于问题表中没有课程名称字段,所以通过
课程ID获取课程名字,课程ID可以从Feign获取
QuestionAdminPageQuery.getCourseName
是课程名称的关键字
课程ID可以从Feign获取:接受课程关键字,搜ES
public PageDTO<QuestionAdminVO> getInterationQuestionByAdminPage(QuestionAdminPageQuery pageQuery) {
// 如果用户传了课程名称参数,则从es中获取该名称对应的课程id
List<Long> courseIdList = null;
if (StringUtils.isNotBlank(pageQuery.getCourseName())) {
// feign远程调用,从es中获取该名称对应的课程id
courseIdList = searchClient.queryCoursesIdByName(pageQuery.getCourseName());
// 判断查询结果是否为空
if (CollUtil.isEmpty(courseIdList)) {
return PageDTO.empty(0L, 0L);
}
}
// 查询互动问题表
Page<InteractionQuestion> questionPage = lambdaQuery()
.eq(pageQuery.getStatus() != null, InteractionQuestion::getStatus, pageQuery.getStatus())
.ge(pageQuery.getBeginTime() != null, InteractionQuestion::getCreateTime, pageQuery.getBeginTime())
.le(pageQuery.getEndTime() != null, InteractionQuestion::getCreateTime, pageQuery.getEndTime())
.in(!CollUtil.isEmpty(courseIdList), InteractionQuestion::getCourseId, courseIdList) // 实现课程名称模糊查询
.page(pageQuery.toMpPageDefaultSortByCreateTimeDesc());
// 查询到的列表为空,则返回空集
List<InteractionQuestion> records = questionPage.getRecords();
if (CollUtil.isEmpty(records)) {
return PageDTO.of(questionPage, Collections.emptyList());
}
// 这里用for循环而不是Stream流,减少循环次数
Set<Long> userIds = new HashSet<>();
Set<Long> courseIds = new HashSet<>();
Set<Long> chapterAndSections = new HashSet<>();
for (InteractionQuestion question : records) {
userIds.add(question.getUserId());
courseIds.add(question.getCourseId());
chapterAndSections.add(question.getChapterId());
chapterAndSections.add(question.getSectionId());
}
// feign远程调用用户服务,获取用户信息
List<UserDTO> userDTOS = userClient.queryUserByIds(userIds);
if (CollUtil.isEmpty(userDTOS)) {
throw new BizIllegalException("用户不存在");
}
Map<Long, UserDTO> userMap = userDTOS.stream().collect(Collectors.toMap(UserDTO::getId, userDTO -> userDTO));
// feign远程调用课程服务,获取课程信息
List<CourseSimpleInfoDTO> courseDTOs = courseClient.getSimpleInfoList(courseIds);
if (CollUtil.isEmpty(courseDTOs)) {
throw new BizIllegalException("课程不存在");
}
Map<Long, CourseSimpleInfoDTO> courseMap = courseDTOs.stream()
.collect(Collectors.toMap(CourseSimpleInfoDTO::getId, courseDTO -> courseDTO));
// feign远程调用课程服务,获取章节信息
List<CataSimpleInfoDTO> catalogueDTOs = catalogueClient.batchQueryCatalogue(chapterAndSections);
if (CollUtil.isEmpty(catalogueDTOs)) {
throw new BizIllegalException("章节不存在");
}
// 封装为章节id,章节名称(需要根据章节id赋值章节名称)
Map<Long, String> catalogueMap = catalogueDTOs.stream()
.collect(Collectors.toMap(CataSimpleInfoDTO::getId, CataSimpleInfoDTO::getName));
// 封装VO并返回
List<QuestionAdminVO> voList = new ArrayList<>();
for (InteractionQuestion record : records) {
QuestionAdminVO questionAdminVO = BeanUtils.copyBean(record, QuestionAdminVO.class);
UserDTO userDTO = userMap.get(record.getUserId());
if (userDTO != null) {
questionAdminVO.setUserName(userDTO.getName()); // 用户昵称
}
CourseSimpleInfoDTO courseDTO = courseMap.get(record.getCourseId());
if (courseDTO != null) {
questionAdminVO.setCourseName(courseDTO.getName()); // 课程名称
// 获取课程的三级分类id,根据三级分类id拼接分类名称
String categoryName = categoryCache.getCategoryNames(courseDTO.getCategoryIds());
questionAdminVO.setCategoryName(categoryName); // 课程所述分类名称
}
// 使用getOrDefault防止异常
questionAdminVO.setChapterName(catalogueMap.getOrDefault(record.getChapterId(), "")); // 章节名称
questionAdminVO.setSectionName(catalogueMap.getOrDefault(record.getSectionId(), "")); // 小节名称
voList.add(questionAdminVO);
}
return PageDTO.of(questionPage, voList);
}
三级分类
表里设置parent_id代表上级是谁
都是IT-互联网下面的二级分类(红框里的)
因为分类信息的改动量比较小,一般都不会动了,所以就缓存起来
- 课程分类数据在很多业务中都需要查询,这样的数据如此频繁的查询,有没有性能优化的办法呢?
3.6.5.2.多级缓存
相信很多同学都能想到借助于Redis缓存来提高性能,减少数据库压力。非常好!不过,Redis虽然能提高性能,但每次查询缓存还是会增加网络带宽消耗,也会存在网络延迟。
而分类数据具备两大特点:
- 数据量小
- 长时间不会发生变化。
像这样的数据,除了建立Redis缓存以外,还非常适合做本地缓存(Local Cache)。这样就可以形成多级缓存机制:
- 数据查询时优先查询本地缓存
- 本地缓存不存在,再查询Redis缓存
- Redis不存在,再去查询数据库。
本地缓存简单来说就是JVM内存的缓存,比如你建立一个HashMap,把数据库查询的数据存入进去。以后优先从这个HashMap查询,一个本地缓存就建立好了。
3.6.5.3.Caffeine
当然,我们真正创建本地缓存的时候并不是直接使用HashMap之类的集合,因为维护起来不太方便。而且内存淘汰机制实现起来也比较麻烦。
所以,我们会使用成熟的框架来完成,比如Caffeine:
Caffeine是一个基于Java8开发的,提供了近乎最佳命中率的高性能的本地缓存库。目前Spring内部的缓存使用的就是Caffeine。
第二次get就不会执行了
Caffeine提供了三种缓存驱逐策略:
- 基于容量:设置缓存的数量上限
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(1) // 设置缓存大小上限为 1
.build();
- 基于时间:设置缓存的有效时间
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
// 设置缓存有效期为 10 秒,从最后一次写入开始计时
.expireAfterWrite(Duration.ofSeconds(10))
.build();
Caffeine.newBuilder()
.initialCapacity(1) // 初始容量 缓存初始化时会分配足够的内存来存储1个键值对。
.maximumSize(10_000) // 最大容量 缓存最多可以存储10,000个键值对。
.expireAfterWrite(Duration.ofMinutes(30)) // 指定了缓存项在写入后多长时间过期。Duration.ofMinutes(30)是一个静态方法,用于创建一个表示30分钟的时间持续对象
.build();
和上面明星例子一样,如果缓存没有则远程调用获取放到缓存中 下次30分钟查 直接返回【一级分类id、二级分类id、三级分类id】:
public Map<Long, CategoryBasicDTO> getCategoryMap() {
return categoryCaches.get("CATEGORY", key -> {
// 1.从CategoryClient查询
List<CategoryBasicDTO> list = categoryClient.getAllOfOneLevel();
if (list == null || list.isEmpty()) {
return CollUtils.emptyMap();
}
return list.stream().collect(Collectors.toMap(CategoryBasicDTO::getId, Function.identity()));
});
}
拼接三级分类名称,用/分隔:
/**
* 根据三级分类id拼接三级分类名称
* @param ids 一级分类id、二级分类id、三级分类id
* @return 拼接三级分类名称,用/分隔
*/
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();
}
调用:
questionAdminVO.setCourseName(courseDTO.getName()); // 课程名称
// 获取课程的三级分类id,根据三级分类id拼接分类名称
String categoryName = categoryCache.getCategoryNames(courseDTO.getCategoryIds());
questionAdminVO.setCategoryName(categoryName); // 课程所述分类名称
启动缓存生效这一系列流程:
Feign客户端的实现类是由Feign在运行时动态生成的,你不需要手动编写实现类。只要你的项目配置正确,Feign会自动处理接口的实现,并通过HTTP请求调用远程服务。
4.评论相关接口
目标效果
回答
评论是 回答下面的
新增回答或评论
@Data
@ApiModel(description = "互动回答信息")
public class ReplyDTO {
@ApiModelProperty("回答内容")
@NotNull(message = "回答内容不能为空")
private String content;
@ApiModelProperty("是否匿名提问")
private Boolean anonymity;
@ApiModelProperty("互动问题id")
@NotNull(message = "问题id不能为空")
private Long questionId;
// 该字段为null,表示是回答;否则表示评论
@ApiModelProperty("回复的上级回答id,没有可不填")
private Long answerId;
@ApiModelProperty("回复的目标回复id,没有可不填")
private Long targetReplyId;
@ApiModelProperty("回复的目标用户id,没有可不填")
private Long targetUserId;
@ApiModelProperty("标记是否是学生提交的回答,默认true")
private Boolean isStudent = true;
}
@Transactional
public void addReply(ReplyDTO replyDTO) {
// 拷贝实体
InteractionReply reply = BeanUtil.toBean(replyDTO, InteractionReply.class);
if (reply.getAnswerId() == null) { // 当前是回答的话,不需要target_user_id字段
reply.setTargetUserId(null);
}
// 获取当前登录用户
Long userId = UserContext.getUser();
reply.setUserId(userId);
// 保存评论或回答
this.save(reply);
// 查询关联的问题
InteractionQuestion question = questionMapper.selectById(reply.getQuestionId());
if (question == null) {
throw new BizIllegalException("参数异常");
}
// 根据answerId是否为null判断是回答还是评论,如果是需要在`interaction_question`中记录最新一次回答的id
if (reply.getAnswerId() == null) { // answerId为null表示当前是回答
question.setLatestAnswerId(reply.getId()); // 更新问题的最新回答id
question.setAnswerTimes(question.getAnswerTimes() + 1); // 该问题的回答数量+1
} else { // 如果是评论
// 获取评论关联的回答
InteractionReply interactionReply = this.getById(reply.getAnswerId());
interactionReply.setReplyTimes(interactionReply.getReplyTimes() + 1); // 该回答的评论数量+1
// 更新评论关联的回答
this.updateById(interactionReply);
}
// 如果是学生提交,则需要更新问题状态为未查看
if (replyDTO.getIsStudent()) {
question.setStatus(QuestionStatus.UN_CHECK);
}
// 更新问题
questionMapper.updateById(question);
// 发送MQ消息,新增积分
rabbitMqHelper.send(MqConstants.Exchange.LEARNING_EXCHANGE,
MqConstants.Key.WRITE_REPLY,
SignInMessage.of(userId,5)); // 一个问题+5积分
}
原文地址:https://blog.csdn.net/juli_eyre/article/details/145144697
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!