酷站海洛,门户网站开发用什么框架好,网站建设策划书范本,大连seo网站文章目录 day05-问答系统表 用户端分页查询问题目标效果代码实现 3.6.管理端分页查询问题ES相关 管理端互动问题分页实现三级分类3.6.5.2.多级缓存3.6.5.3.CaffeineTODO#xff1a;使用Caffeine作为本地缓存#xff0c;另外使用redis或者memcache作为分布式缓存#xff0c;构… 文章目录 day05-问答系统表 用户端分页查询问题目标效果代码实现 3.6.管理端分页查询问题ES相关 管理端互动问题分页实现三级分类3.6.5.2.多级缓存3.6.5.3.CaffeineTODO使用Caffeine作为本地缓存另外使用redis或者memcache作为分布式缓存构造多级缓存体系 4.评论相关接口目标效果新增回答或评论 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 b0 COMMENT 是否匿名默认false,hidden bit(1) NOT NULL DEFAULT b0 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)
) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci ROW_FORMATDYNAMIC 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 b0 COMMENT 是否被隐藏默认false,anonymity bit(1) NOT NULL DEFAULT b0 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
) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci ROW_FORMATDYNAMIC COMMENT互动问题的回答或评论;KEY 关键字用于定义索引而 USING BTREE 是一个可选的子句用于显式指定索引的存储类型。如果不指定 USING BTREEMySQL 会默认使用 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)ListLong 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 PageDTOCourseVO queryCoursesForPortal(CoursePageQuery query){return searchService.queryCoursesForPortal(query);}ApiIgnoreGetMapping(/name)public ListLong queryCoursesIdByName(RequestParam(keyword) String keyword){return searchService.queryCoursesIdByName(keyword);}管理端互动问题分页实现
管理端互动问题分页: Admin可以通过关键字搜索课程但由于问题表中没有课程名称字段所以通过 课程ID获取课程名字课程ID可以从Feign获取 QuestionAdminPageQuery.getCourseName是课程名称的关键字 课程ID可以从Feign获取接受课程关键字搜ES
public PageDTOQuestionAdminVO getInterationQuestionByAdminPage(QuestionAdminPageQuery pageQuery) {// 如果用户传了课程名称参数则从es中获取该名称对应的课程idListLong courseIdList null;if (StringUtils.isNotBlank(pageQuery.getCourseName())) {// feign远程调用从es中获取该名称对应的课程idcourseIdList searchClient.queryCoursesIdByName(pageQuery.getCourseName());// 判断查询结果是否为空if (CollUtil.isEmpty(courseIdList)) {return PageDTO.empty(0L, 0L);}}// 查询互动问题表PageInteractionQuestion 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());// 查询到的列表为空则返回空集ListInteractionQuestion records questionPage.getRecords();if (CollUtil.isEmpty(records)) {return PageDTO.of(questionPage, Collections.emptyList());}// 这里用for循环而不是Stream流减少循环次数SetLong userIds new HashSet();SetLong courseIds new HashSet();SetLong chapterAndSections new HashSet();for (InteractionQuestion question : records) {userIds.add(question.getUserId());courseIds.add(question.getCourseId());chapterAndSections.add(question.getChapterId());chapterAndSections.add(question.getSectionId());}// feign远程调用用户服务获取用户信息ListUserDTO userDTOS userClient.queryUserByIds(userIds);if (CollUtil.isEmpty(userDTOS)) {throw new BizIllegalException(用户不存在);}MapLong, UserDTO userMap userDTOS.stream().collect(Collectors.toMap(UserDTO::getId, userDTO - userDTO));// feign远程调用课程服务获取课程信息ListCourseSimpleInfoDTO courseDTOs courseClient.getSimpleInfoList(courseIds);if (CollUtil.isEmpty(courseDTOs)) {throw new BizIllegalException(课程不存在);}MapLong, CourseSimpleInfoDTO courseMap courseDTOs.stream().collect(Collectors.toMap(CourseSimpleInfoDTO::getId, courseDTO - courseDTO));// feign远程调用课程服务获取章节信息ListCataSimpleInfoDTO catalogueDTOs catalogueClient.batchQueryCatalogue(chapterAndSections);if (CollUtil.isEmpty(catalogueDTOs)) {throw new BizIllegalException(章节不存在);}// 封装为章节id章节名称需要根据章节id赋值章节名称MapLong, String catalogueMap catalogueDTOs.stream().collect(Collectors.toMap(CataSimpleInfoDTO::getId, CataSimpleInfoDTO::getName));// 封装VO并返回ListQuestionAdminVO 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提供了三种缓存驱逐策略
基于容量设置缓存的数量上限
// 创建缓存对象
CacheString, String cache Caffeine.newBuilder().maximumSize(1) // 设置缓存大小上限为 1.build();基于时间设置缓存的有效时间
// 创建缓存对象
CacheString, 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 MapLong, CategoryBasicDTO getCategoryMap() {return categoryCaches.get(CATEGORY, key - {// 1.从CategoryClient查询ListCategoryBasicDTO 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(ListLong ids) {if (ids null || ids.size() 0) {return ;}// 1.读取分类缓存MapLong, 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); // 课程所述分类名称SpringBoot的自动加载机制启动缓存生效这一系列流程 Feign客户端的实现类是由Feign在运行时动态生成的你不需要手动编写实现类。只要你的项目配置正确Feign会自动处理接口的实现并通过HTTP请求调用远程服务。 TODO使用Caffeine作为本地缓存另外使用redis或者memcache作为分布式缓存构造多级缓存体系 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中记录最新一次回答的idif (reply.getAnswerId() null) { // answerId为null表示当前是回答question.setLatestAnswerId(reply.getId()); // 更新问题的最新回答idquestion.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积分}