当前位置: 首页 > news >正文

day05-问答系统

一个人学习总是孤独的,而且往往难以坚持。

所以我们的系统设计了一些学习辅助的功能,增强学习的氛围感。包括:

  • 互动问答系统

  • 学习笔记系统

  • 学习评测系统

  • 学习积分系统

  • 榜单排名系统

通过这套系统让用户感觉到自己不是一个人在学习,有互助、有竞争、有评测,刺激用户持续学习,提升学习效果和用户粘度。

这套系统中包含了很多企业中非常实用的解决方案和技术手段,可以为大家以后的工作提供很大帮助。例如:

  • 互动问答功能:在社交类型、学习类型的互联网项目中都有用到

  • 点赞功能:电商、社交、学习等等都会用到

  • 积分系统:电商、社交、学习等项目中用到

  • 排行榜系统:游戏、社交、学习等项目中都有用到

  • 学习评测系统:考试、学习类型的项目会用到

在这些解决方案中你能解锁Redis、MQ等热门中间件的各种各样的使用方式。

今天我们首先来看看互动问答系统的设计与实现。

1.需求分析

经过几天学习,相信大家对于业务开发已经轻车熟路了,整体流程与以往一样:

  • 需求和原型图分析

  • 接口统计和设计

  • 数据结构设计

  • 接口的实现

1.1.产品原型

我们首先看看与互动问答有关的原型页面。

1.1.1.课程详情页

在用户已经登录的情况下,如果用户购买了课程,在课程详情页可以看到一个互动问答的选项卡:

问答选项卡如下:

点击提问或编辑按钮会进入问题编辑页面:

点击某个问题,则会进入问题详情页面:

1.1.2.视频学习页

另外,在视频学习页面中同样可以看到互动问答功能:

这个页面与课程详情页功能类似,只不过是在观看视频的过程中操作。用户产生学习疑问是可以快速提问,不用退回到课程详情页,用户体验较好。

1.1.3.管理端问答管理页

除了用户端以外,管理端也可以管理互动问答,首先是一个列表页:

点击查看按钮,会进入一个问题详情页面:

继续点击查看更多按钮,可以进入回答详情页:

1.1.4.流程总结

整体来说,流程是这样的:

  • 学员在学习的过程中可以随时提问问题

  • 老师、其他学员都可以回答问题

  • 老师、学员也都可以对回答多次回复

  • 老师、学员也都可以对评论多次回复

  • 老师可以在管理端管理问题、回答、评论的状态

业务流程并不复杂。

1.2.接口统计

理论上我们应该先设计所有接口,再继续设计接口对应的表结构。不过由于接口较多,这里我们先对接口做简单统计。然后直接设计数据库,最后边设计接口,边实现接口。

1.2.1.问题的CRUD

首先第一个页面,列表展示页:

结合原型设计图我们可以看到这里包含4个接口:

  • 带条件过滤的分页查询

  • 新增提问

  • 修改提问

  • 删除提问

这些都是基本的CRUD,应该不难。

1.2.2.问题的回答和评论

进入问答详情页再看:

可以看到页面中包含5个接口:

  • 根据id查询问题详情

  • 分页查询问题下的所有回答

  • 分页查询回答下的评论

  • 点赞/取消点赞某个回答或评论

  • 回答某个提问、评论他人回答

除了点赞功能外,其它接口也都是基本的增删改查,并不复杂。

1.2.3.管理端接口

刚才分析的都是用户端的相关接口,这些接口部分可以与管理端共用,但管理端也有自己的特有需求。

管理端也可以分页查询问题列表,而且过滤条件、查询结果会有很大不同:

比较明显的有两个接口:

  • 管理端分页查询问题列表:与用户端分页查询不通用,功能更复杂,查询条件更多

  • 隐藏或显示指定问题

除此以外,这里有一个问题状态字段,表示管理员是否查看了该问题以及问题中的回答。默认是未查看状态;当管理员点击查看后,状态会变化为已查看;当学员再次回答或评论,状态会再次变为未查看。

因此,需要注意的是:

  • 每当用户点击查看按钮,需要根据根据id查询问题详情,此时应标记问题状态为已查看

  • 每当学员回答或评论时,需要将问题标记为未查看

管理端也会有回答列表、评论列表。另外,回答和评论同样有隐藏功能。

问题详情和回答列表:

还有评论列表:

总结一下,回答和评论包含的接口有:

  • 管理端根据id查询问题详情

  • 分页查询问题下的回答

  • 分页查询回答下的评论

  • 点赞/取消点赞某个回答或评论

  • 隐藏/显示指定回答或评论

  • 回答某个提问、评论他人回答、评论(与用户端共用)

1.2.4.总结

综上,与问答系统有关的接口有:

2.数据结构

从原型图不难看出,这部分功能主要涉及两个实体:

  • 问题

  • 回答/评论:回答、评论可以看做一类实体

因此核心要设计的就是这两张表。

2.1.ER图

2.1.1.问题

首先是问题,通过新增提问的表单可以看出问题包含的属性:

基本属性:

  • 标题

  • 描述

关联信息:

  • 用户id:也就是提问的人

  • 课程id

  • 章id

  • 节id

功能字段:

  • 是否是匿名

另外,在问题列表中,需要知道问题最新的一个回答的信息:

如果每次分页查询问题的时候再去统计最新的回答是哪个,效率比较低。我们可以直接在每次有新回答时将这个id记录到问题表中。因此问题表中需要添加这样一个字段:

  • 最新一次回答的id

除了这些字段以外,管理端分页查询问题列表时也有一些功能字段:

功能字段:

  • 问题下的回答数量

  • 用户端显示状态:是否被隐藏

  • 问题状态:管理端是否已经查看

综上,问题的ER图如下:

2.1.2.回答、评论

回答和评论的属性基本一致,差别就是:

  • 回答的对象是问题

  • 评论的对象是其它回答或评论

来看下原型图:

首先是基本属性:

  • 回答的内容

功能字段:

  • 是否是匿名

  • 点赞数量

然后是关联信息:

  • 用户id:也就是回答的人

  • 问题id:无论是回答、评论,都属于某个问题

接下来是评论的特有属性:

  • 回答id:一个回答下会有很多评论,评论之间也会相互评论,但我们把回答下所有评论作为一层来展示。因此该回答下的所有评论都应记住所属的回答的id

  • 目标用户id:评论针对的目标用户,页面显示为 张三评论了李四

  • 目标评论id:评论针对的目标评论的id

综上,评论的ER图为:

2.2.数据库表

结合ER图,表结构就非常清楚了,会包含两张表:

  • 问题表

  • 回复表:回答和评论都是回复,在一张表

2.2.1.问题表

首先是问题表:

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='互动提问的问题表';

2.2.3.回答或评论

回答和评论合为一张表,称为评论表:

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='互动问题的回答或评论';

2.3.代码生成

接下来,利用MP插件自动代码生成对应的代码。不过在这之前,不要忘了创建新的功能分支。这里我们是互动问答功能,我们在dev分支基础上创建一个问答分支:feature-qa

然后再生成代码即可。

2.3.1.controller

注意把controller路径修改为Restful风格:

2.3.2.ID策略

问题和评论的id都采用雪花算法:

评论:

2.3.3.状态枚举

问题存在已查看和未查看两种状态,我们定义出一个枚举来标示:

具体代码:

@Getter
public enum QuestionStatus implements BaseEnum {UN_CHECK(0, "未查看"),CHECKED(1, "已查看"),;@JsonValue@EnumValueint value;String desc;QuestionStatus(int value, String desc) {this.value = value;this.desc = desc;}@JsonCreator(mode = JsonCreator.Mode.DELEGATING)public static QuestionStatus of(Integer value){if (value == null) {return null;}for (QuestionStatus status : values()) {if (status.equalsValue(value)) {return status;}}return null;}
}

然后把InteractionQuestion类中的状态修改为枚举类型:

3.问题相关接口

问题相关接口在管理端和用户端存在一些差异,在设计接口时一定要留意。另外,此处我们只带大家实现其中的部分接口:

  • 新增互动问题

  • 用户端分页查询问题

  • 根据id查询问题详情

  • 管理端分页查询问题

其它的接口留给大家作为练习,包括:

  • 管理端根据id查询问题详情

  • 修改问题

  • 删除问题

  • 管理端隐藏或显示问题

  • 新增回答或评论

  • 分页查询回答或评论

  • 管理端分页查询回答或评论

  • 管理端隐藏或显示回答、评论

3.1.新增问题

3.1.1.接口分析

首先还是看原型图,新增的表单如下:

通过新增的问题的表单即可分析出接口的请求参数信息了,然后按照Restful的风格设计即可:

3.1.2.实体类

新增业务中无返回值,只需要设计出入参对应的DTO即可,在课前资料中已经提供好了:

复制到tj-learning模块下的domain下的dto包下:

3.1.3.代码实现

首先是tj-learning中的InteractionQuestionController

@RestController
@RequestMapping("/questions")
@RequiredArgsConstructor
public class InteractionQuestionController {private final IInteractionQuestionService questionService;@ApiOperation("新增提问")@PostMappingpublic void saveQuestion(@Valid @RequestBody QuestionFormDTO questionDTO){questionService.saveQuestion(questionDTO);}
}

然后是tj-learning中的IInteractionQuestionService接口:

public interface IInteractionQuestionService extends IService<InteractionQuestion> {void saveQuestion(QuestionFormDTO questionDTO);
}

最后是tj-learning中的InteractionQuestionServiceImpl实现类:

@Service
@RequiredArgsConstructor
public class InteractionQuestionServiceImpl extends ServiceImpl<InteractionQuestionMapper, InteractionQuestion> implements IInteractionQuestionService {@Override@Transactionalpublic void saveQuestion(QuestionFormDTO questionDTO) {// 1.获取登录用户Long userId = UserContext.getUser();// 2.数据转换InteractionQuestion question = BeanUtils.toBean(questionDTO, InteractionQuestion.class);// 3.补充数据question.setUserId(userId);// 4.保存问题save(question);}
}

3.2.修改问题(练习)

3.2.1.接口分析

修改与新增表单基本类似,此处不再分析。我们可以参考新增的接口,然后按照Restful的风格设计为更新即可:

虽然修改问题时提交的JSON参数会少一些,不过依然可以沿用新增时的DTO.

3.2.2.代码实现(练习)

    @Overridepublic void updateQuestion(Long id, QuestionFormDTO questionFormDTO) {Long userId = UserContext.getUser();boolean success = lambdaUpdate().eq(InteractionQuestion::getUserId, userId).eq(InteractionQuestion::getId, id).set(InteractionQuestion::getTitle, questionFormDTO.getTitle()).set(InteractionQuestion::getDescription, questionFormDTO.getDescription()).set(InteractionQuestion::getAnonymity, questionFormDTO.getAnonymity()).update();if(!success){throw new DbException("修改问题失败");}}

3.3.用户端分页查询问题

3.1.1.接口分析

先看原型图:

这就是一个典型的分页查询。主要分析请求参数和返回值就行了。

请求参数就是过滤条件,页面可以看到的条件有:

  • 分页条件

  • 全部回答/我的回答:也就是要不要基于用户id过滤

  • 课程id:隐含条件,因为问题列表是在某课程详情页面查看的,所以一定要以课程id为条件

  • 章节id:可选条件,当用户点击小节时传递

返回值格式,从页面可以看到属性有:

  • 是否匿名:如果提交问题是选择了匿名,则页面不能展示用户信息

  • 用户id:匿名则不显示

  • 用户头像:匿名则不显示

  • 用户名称:匿名则不显示

  • 问题标题

  • 提问时间

  • 回答数量

  • 最近一次回答的信息:

    • 回答人名称

    • 回答内容

综上,按照Restful来设计接口,信息如下:

3.1.2.实体类

首先是请求参数,查询类型我们定义为Query,在课前资料中已经提供了:

需要注意的是,QuestionPageQuery是继承自tj-common中通用的PageQuery,这样就无需重复定义分页查询参数了:

然后是返回值,页面视图对象,我们定义为VO,在课前资料中已经提供了:

我们将VO和Query实体分别放到domain包的vo包和query包下:

3.1.3.声明接口

首先是tj-learning中的com.tianji.learning.controller.InteractionQuestionController

package com.tianji.learning.controller;import com.tianji.common.domain.dto.PageDTO;
import com.tianji.learning.domain.query.QuestionPageQuery;
import com.tianji.learning.domain.vo.QuestionVO;
import com.tianji.learning.service.IInteractionQuestionService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;/*** <p>* 互动提问的问题表 前端控制器* </p>*/
@RestController
@RequestMapping("/questions")
@Api(tags = "互动问答相关接口")
@RequiredArgsConstructor
public class InteractionQuestionController {private final IInteractionQuestionService questionService;@ApiOperation("分页查询互动问题")@GetMapping("page")public PageDTO<QuestionVO> queryQuestionPage(QuestionPageQuery query){return questionService.queryQuestionPage(query);}}

然后是tj-learning中的com.tianji.learning.service.IInteractionQuestionService接口:

package com.tianji.learning.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.tianji.common.domain.dto.PageDTO;
import com.tianji.learning.domain.query.QuestionPageQuery;
import com.tianji.learning.domain.vo.QuestionVO;/*** <p>* 互动提问的问题表 服务类* </p>*/
public interface IInteractionQuestionService extends IService<InteractionQuestion> {PageDTO<QuestionVO> queryQuestionPage(QuestionPageQuery query);
}

最后是tj-learning中的com.tianji.learning.service.impl.InteractionQuestionServiceImpl实现类:

package com.tianji.learning.service.impl;import com.tianji.learning.domain.query.QuestionPageQuery;
import com.tianji.learning.domain.vo.QuestionVO;
import com.tianji.learning.service.IInteractionQuestionService;
import org.springframework.stereotype.Service;import java.time.LocalDateTime;
import java.util.*;/*** <p>* 互动提问的问题表 服务实现类* </p>*/
@Service
@RequiredArgsConstructor
public class InteractionQuestionServiceImpl extends ServiceImpl<InteractionQuestionMapper, InteractionQuestion> implements IInteractionQuestionService {@Overridepublic PageDTO<QuestionVO> queryQuestionPage(QuestionPageQuery query) {return null;}
}

3.1.4.查询用户信息

由于页面VO中需要提问者信息、最近一次回答信息,都需要查询出用户昵称、头像等。而数据库中仅仅保存了提问人的id、回答人的id。

这就要求我们能够根据用户id去查询出用户的详细信息。

而用户信息全部都在tj-user模块对应的user-service服务中。所以我们需要远程调用user-service以获取这些数据。

好在user-service已经提供了查询用户的Feign客户端,并且统一定义到了tj-api模块中:

其中有这样的一个API:

恰好就能实现根据id集合查询用户信息集合的功能。

而且返回的UserDTO中数据非常丰富,完全能够满足我们的需要:

3.1.5.实现查询逻辑

查询的实现逻辑如下:

package com.tianji.learning.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.api.cache.CategoryCache;
import com.tianji.api.client.user.UserClient;
import com.tianji.api.dto.user.UserDTO;
import com.tianji.common.domain.dto.PageDTO;
import com.tianji.common.exceptions.BadRequestException;
import com.tianji.common.utils.BeanUtils;
import com.tianji.common.utils.CollUtils;
import com.tianji.common.utils.StringUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.learning.domain.po.InteractionQuestion;
import com.tianji.learning.domain.po.InteractionReply;
import com.tianji.learning.domain.query.QuestionPageQuery;
import com.tianji.learning.domain.vo.QuestionVO;
import com.tianji.learning.mapper.InteractionQuestionMapper;
import com.tianji.learning.mapper.InteractionReplyMapper;
import com.tianji.learning.service.IInteractionQuestionService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;/*** <p>* 互动提问的问题表 服务实现类* </p>** @author 虎哥*/
@Service
@RequiredArgsConstructor
public class InteractionQuestionServiceImpl extends ServiceImpl<InteractionQuestionMapper, InteractionQuestion> implements IInteractionQuestionService {private final InteractionReplyMapper replyMapper;private final UserClient userClient;@Overridepublic PageDTO<QuestionVO> queryQuestionPage(QuestionPageQuery query) {// 1.参数校验,课程id和小节id不能都为空Long courseId = query.getCourseId();Long sectionId = query.getSectionId();if (courseId == null && sectionId == null) {throw new BadRequestException("课程id和小节id不能都为空");}// 2.分页查询Page<InteractionQuestion> page = lambdaQuery().select(InteractionQuestion.class, info -> !info.getProperty().equals("description")).eq(query.getOnlyMine(), InteractionQuestion::getUserId, UserContext.getUser()).eq(courseId != null, InteractionQuestion::getCourseId, courseId).eq(sectionId != null, InteractionQuestion::getSectionId, sectionId).eq(InteractionQuestion::getHidden, false).page(query.toMpPageDefaultSortByCreateTimeDesc());List<InteractionQuestion> records = page.getRecords();if (CollUtils.isEmpty(records)) {return PageDTO.empty(page);}// 3.根据id查询提问者和最近一次回答的信息Set<Long> userIds = new HashSet<>();Set<Long> answerIds = new HashSet<>();// 3.1.得到问题当中的提问者id和最近一次回答的idfor (InteractionQuestion q : records) {if(!q.getAnonymity()) { // 只查询非匿名的问题userIds.add(q.getUserId());}answerIds.add(q.getLatestAnswerId());}// 3.2.根据id查询最近一次回答answerIds.remove(null);Map<Long, InteractionReply> replyMap = new HashMap<>(answerIds.size());if(CollUtils.isNotEmpty(answerIds)) {List<InteractionReply> replies = replyMapper.selectBatchIds(answerIds);for (InteractionReply reply : replies) {replyMap.put(reply.getId(), reply);if(!reply.getAnonymity()){ // 匿名用户不做查询userIds.add(reply.getUserId());}}}// 3.3.根据id查询用户信息(提问者)userIds.remove(null);Map<Long, UserDTO> userMap = new HashMap<>(userIds.size());if(CollUtils.isNotEmpty(userIds)) {List<UserDTO> users = userClient.queryUserByIds(userIds);userMap = users.stream().collect(Collectors.toMap(UserDTO::getId, u -> u));}// 4.封装VOList<QuestionVO> voList = new ArrayList<>(records.size());for (InteractionQuestion r : records) {// 4.1.将PO转为VOQuestionVO vo = BeanUtils.copyBean(r, QuestionVO.class);vo.setUserId(null);voList.add(vo);// 4.2.封装提问者信息if(!r.getAnonymity()){UserDTO userDTO = userMap.get(r.getUserId());if (userDTO != null) {vo.setUserId(userDTO.getId());vo.setUserName(userDTO.getName());vo.setUserIcon(userDTO.getIcon());}}// 4.3.封装最近一次回答的信息InteractionReply reply = replyMap.get(r.getLatestAnswerId());if (reply != null) {vo.setLatestReplyContent(reply.getContent());if(!reply.getAnonymity()){// 匿名用户直接忽略UserDTO user = userMap.get(reply.getUserId());vo.setLatestReplyUser(user.getName());}}}return PageDTO.of(page, voList);}
}

3.4.根据id查询问题详情

3.4.1.接口分析

先看下详情页原型图:

由此可以看出详情页所需要的信息相比分页时,主要多了问题详情,主要字段有:

  • 是否匿名

  • 用户id:匿名则不显示

  • 用户头像:匿名则不显示

  • 用户 名称:匿名则不显示

  • 问题标题

  • 提问时间

  • 回答数量

  • 问题描述详情

而请求参数则更加简单了,就是问题的id

然后,再按照Restful风格设计,接口就出来了:

3.4.2.实体类

既然仅仅比分页时多了一个字段,我们可以沿用之前分页查询时的VO对象,添加一个新属性即可:

3.4.3.代码实现

首先是tj-learning中的InteractionQuestionController

@ApiOperation("根据id查询问题详情")
@GetMapping("/{id}")
public QuestionVO queryQuestionById(@ApiParam(value = "问题id", example = "1") @PathVariable("id") Long id){return questionService.queryQuestionById(id);
}

然后是tj-learning中的IInteractionQuestionService接口:

QuestionVO queryQuestionById(Long id);

最后是tj-learning中的InteractionQuestionServiceImpl实现类:

@Override
public QuestionVO queryQuestionById(Long id) {// 1.根据id查询数据InteractionQuestion question = getById(id);// 2.数据校验if(question == null || question.getHidden()){// 没有数据或者是被隐藏了return null;}// 3.查询提问者信息UserDTO user = null;if(!question.getAnonymity()){user = userClient.queryUserById(question.getUserId());}// 4.封装VOQuestionVO vo = BeanUtils.copyBean(question, QuestionVO.class);if (user != null) {vo.setUserName(user.getName());vo.setUserIcon(user.getIcon());}return vo;
}

3.5.删除我的问题(练习)

3.5.1.接口分析

用户可以删除自己提问的问题,如图:

要注意的是,当用户删除某个问题时,也需要删除问题下的回答、评论。

整体业务流程如下:

  • 查询问题是否存在

  • 判断是否是当前用户提问的

  • 如果不是则报错

  • 如果是则删除问题

  • 然后删除问题下的回答及评论

接口信息如下:

  • 接口地址:/questions/{id}

  • 请求方式:DELETE

  • 请求参数: 基于路径占位符,问题id

3.5.2.代码实现(练习)

    @Overridepublic void deleteQuestionById(Long id) {InteractionQuestion question = lambdaQuery().eq(InteractionQuestion::getId, id).one();if(question != null){if(!question.getUserId().equals(UserContext.getUser())){throw new BadRequestException("当前问题不是当前登录用户提出的");}// 应该在这里执行删除操作boolean success = removeById(id);if(!success){throw new DbException("删除问题失败");}// 连带下面的回复也删除replyMapper.delete(new LambdaQueryWrapper<InteractionReply>().eq(InteractionReply::getQuestionId, id));}else{throw new BadRequestException("不存在当前问题");}}

3.6.管理端分页查询问题

3.6.1.接口分析

在管理端后台存在问答管理列表页,与用户端类似都是分页查询,但是请求参数和返回值有较大差别:

从请求参数来看,除了分页参数,还包含3个:

  • 问题的查看状态

  • 课程名称

  • 提问时间

从返回值来看,比用户端多了一些字段:

  • 是否匿名: 管理端不关心,全都展示

  • 提问者信息:

    • 用户id

    • 用户头像:匿名则不显示

    • 用户 名称:匿名则不显示

  • 问题标题

  • 提问时间

  • 回答数量

  • 最近一次回答的信息:

    • 回答人名称

    • 回答内容

  • 问题关联的课程名称

  • 问题关联的章、节名称

  • 问题关联课程的分类名称

由于请求入参和返回值与用户端有较大差异,因此我们需要设计一个新的接口:

3.6.2.实体类

与用户端分页查询问题类似,这里也需要定义Query实体、VO实体。课前资料中已经提供好了:

首先是Query:

然后是VO:

3.6.3.接口声明

为了与用户端加以区分,我们定义一个新的controller:

代码如下:

package com.tianji.learning.controller;import com.tianji.common.domain.dto.PageDTO;
import com.tianji.learning.domain.query.QuestionAdminPageQuery;
import com.tianji.learning.domain.vo.QuestionAdminVO;
import com.tianji.learning.service.IInteractionQuestionService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;/*** <p>* 互动提问的问题表 前端控制器* </p>** @author 虎哥*/
@RestController
@RequestMapping("/admin/questions")
@Api(tags = "互动问答相关接口")
@RequiredArgsConstructor
public class InteractionQuestionAdminController {private final IInteractionQuestionService questionService;@ApiOperation("管理端分页查询互动问题")@GetMapping("page")public PageDTO<QuestionAdminVO> queryQuestionPageAdmin(QuestionAdminPageQuery query){return questionService.queryQuestionPageAdmin(query);}
}

然后在tj-learningcom.tianji.learning.service.IInteractionQuestionService中添加方法声明:

PageDTO<QuestionAdminVO> queryQuestionPageAdmin(QuestionAdminPageQuery query);

在实现类com.tianji.learning.service.impl.InteractionQuestionServiceImpl中实现方法:

@Override
public PageDTO<QuestionAdminVO> queryQuestionsPageForAdmin(QuestionAdminPageQuery query) {// TODOreturn null;
}

3.6.4.课程名称模糊搜索

在管理端的查询条件中有一个根据课程名称搜索:

但是,在interaction_question表中,并没有课程名称字段,只有课程id:

那我们该如何实现模糊搜索呢?

在天机学堂项目中,所有上线的课程数据都会存储到Elasticsearch中,方便用户检索课程。并且在tj-search模块中提供了相关的查询接口。

其中就有根据课程名称搜索课程信息的功能,并且这个功能还对外开放了一个Feign客户端方便我们调用:

这个接口的请求参数就是课程名称关键字,其内部会利用elasticsearch的全文检索功能帮我们查询相关课程,并返回课程id集合。这不正是我们所需要的嘛!

因此,如果前端传递了课程名称关键字,我们的搜索流程如下:

  • 首先调用SearchClient接口,根据名称关键字搜索,获取courseId集合

  • 判断结果是否为空

    • 如果为空,直接结束,代表没有搜索到

    • 如果不为空,则把得到的courseId集合作为条件,结合其它条件,分页查询问题即可

3.6.5.课程分类数据

3.6.5.1.查询思路分析

前面分析过,管理端除了要查询到问题,还需要返回问题所属的一系列信息:

这些数据对应到interaction_question表中,只包含一些id字段:

那么我们该如何获取其名称数据呢?

  • 课程名称:根据course_id到课程微服务查询

  • 章节名称:根据chapter_id和section_id到课程微服务查询

  • 分类:未知

  • 提问者名称:根据user_id到用户微服务查询

其中,课程、章节、提问者等信息的查询在以往的业务中我们已经涉及到,不再赘述。但是课程分类信息以前没有查询过。

课程分类在首页就能看到,共分为3级:

每一个课程都与第三级分类关联,因此向上级追溯,也有对应的二级、一级分类。在课程微服务提供的查询课程的接口中,可以看到返回的课程信息中就包含了关联的一级、二级、三级分类:

因此,只要我们查询到了问题所属的课程,就能知道课程关联的三级分类id,接下来只需要根据分类id查询出分类名称即可。

而在course-service服务中提供了一个接口,可以查询到所有的分类:

需要注意的是:这个返回的是所有课程分类的集合,而课程中只包含3个分类id。因此我们需要自己从所有分类集合中找出课程有关的这三个。

分析到这里大家应该知道如何做了。不过这里有一个值得思考的点:

  • 课程分类数据在很多业务中都需要查询,这样的数据如此频繁的查询,有没有性能优化的办法呢?

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。GitHub地址:https://github.com/ben-manes/caffeine

Caffeine的性能非常好,下图是官方给出的几种常见的本地缓存实现方案的性能对比:

可以看到Caffeine的性能遥遥领先!

缓存使用的基本API:

@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);
}

Caffeine既然是缓存的一种,肯定需要有缓存的清除策略,不然的话内存总会有耗尽的时候。

Caffeine提供了三种缓存驱逐策略:

  • 基于容量:设置缓存的数量上限

// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder().maximumSize(1) // 设置缓存大小上限为 1.build();

  • 基于时间:设置缓存的有效时间

// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()// 设置缓存有效期为 10 秒,从最后一次写入开始计时 .expireAfterWrite(Duration.ofSeconds(10)) .build();

  • 基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用。

注意:在默认情况下,当一个缓存元素过期的时候,Caffeine不会自动立即将其清理和驱逐。而是在一次读或写操作后,或者在空闲时间完成对失效数据的驱逐。

3.6.5.4.课程分类的本地缓存

其实,在tj-api模块中,已经定义好了商品分类的本地缓存:

其中CategoryCacheConfig是Caffeine的缓存配置:

package com.tianji.api.config;import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.tianji.api.cache.CategoryCache;
import com.tianji.api.client.course.CategoryClient;
import com.tianji.api.dto.course.CategoryBasicDTO;
import org.springframework.context.annotation.Bean;import java.time.Duration;
import java.util.Map;public class CategoryCacheConfig {/*** 课程分类的caffeine缓存*/@Beanpublic Cache<String, Map<Long, CategoryBasicDTO>> categoryCaches(){return Caffeine.newBuilder().initialCapacity(1) // 容量限制.maximumSize(10_000) // 最大内存限制.expireAfterWrite(Duration.ofMinutes(30)) // 有效期.build();}/*** 课程分类的缓存工具类*/@Beanpublic CategoryCache categoryCache(Cache<String, Map<Long, CategoryBasicDTO>> categoryCaches, CategoryClient categoryClient){return new CategoryCache(categoryCaches, categoryClient);}
}

CategoryCache则是缓存使用的工具类。由于商品分类经常需要根据id查询,因此我根据id查询分类的各种API:

这样,在任何微服务中,只要引入tj-api,我们就能非常方便并且高性能的获取分类名称了。

3.6.5.5.实现分页查询

最终,InteractionQuestionServiceImpl代码如下:

@Override
public PageDTO<QuestionAdminVO> queryQuestionPageAdmin(QuestionAdminPageQuery query) {// 1.处理课程名称,得到课程idList<Long> courseIds = null;if (StringUtils.isNotBlank(query.getCourseName())) {courseIds = searchClient.queryCoursesIdByName(query.getCourseName());if (CollUtils.isEmpty(courseIds)) {return PageDTO.empty(0L, 0L);}}// 2.分页查询Integer status = query.getStatus();LocalDateTime begin = query.getBeginTime();LocalDateTime end = query.getEndTime();Page<InteractionQuestion> page = lambdaQuery().in(courseIds != null, InteractionQuestion::getCourseId, courseIds).eq(status != null, InteractionQuestion::getStatus, status).gt(begin != null, InteractionQuestion::getCreateTime, begin).lt(end != null, InteractionQuestion::getCreateTime, end).page(query.toMpPageDefaultSortByCreateTimeDesc());List<InteractionQuestion> records = page.getRecords();if (CollUtils.isEmpty(records)) {return PageDTO.empty(page);}// 3.准备VO需要的数据:用户数据、课程数据、章节数据Set<Long> userIds = new HashSet<>();Set<Long> cIds = new HashSet<>();Set<Long> cataIds = new HashSet<>();// 3.1.获取各种数据的id集合for (InteractionQuestion q : records) {userIds.add(q.getUserId());cIds.add(q.getCourseId());cataIds.add(q.getChapterId());cataIds.add(q.getSectionId());}// 3.2.根据id查询用户List<UserDTO> users = userClient.queryUserByIds(userIds);Map<Long, UserDTO> userMap = new HashMap<>(users.size());if (CollUtils.isNotEmpty(users)) {userMap = users.stream().collect(Collectors.toMap(UserDTO::getId, u -> u));}// 3.3.根据id查询课程List<CourseSimpleInfoDTO> cInfos = courseClient.getSimpleInfoList(cIds);Map<Long, CourseSimpleInfoDTO> cInfoMap = new HashMap<>(cInfos.size());if (CollUtils.isNotEmpty(cInfos)) {cInfoMap = cInfos.stream().collect(Collectors.toMap(CourseSimpleInfoDTO::getId, c -> c));}// 3.4.根据id查询章节List<CataSimpleInfoDTO> catas = catalogueClient.batchQueryCatalogue(cataIds);Map<Long, String> cataMap = new HashMap<>(catas.size());if (CollUtils.isNotEmpty(catas)) {cataMap = catas.stream().collect(Collectors.toMap(CataSimpleInfoDTO::getId, CataSimpleInfoDTO::getName));}// 4.封装VOList<QuestionAdminVO> voList = new ArrayList<>(records.size());for (InteractionQuestion q : records) {// 4.1.将PO转VO,属性拷贝QuestionAdminVO vo = BeanUtils.copyBean(q, QuestionAdminVO.class);voList.add(vo);// 4.2.用户信息UserDTO user = userMap.get(q.getUserId());if (user != null) {vo.setUserName(user.getName());}// 4.3.课程信息以及分类信息CourseSimpleInfoDTO cInfo = cInfoMap.get(q.getCourseId());if (cInfo != null) {vo.setCourseName(cInfo.getName());vo.setCategoryName(categoryCache.getCategoryNames(cInfo.getCategoryIds()));}// 4.4.章节信息vo.setChapterName(cataMap.getOrDefault(q.getChapterId(), ""));vo.setSectionName(cataMap.getOrDefault(q.getSectionId(), ""));}return PageDTO.of(page, voList);

3.7.管理端隐藏或显示问题(练习)

3.7.1.接口分析

在管理端的互动问题列表中,管理员可以隐藏某个问题,这样就不会在用户端页面展示了:

由于interaction_question表中有一个hidden字段来表示是否隐藏:

因此,本质来说,这个接口是一个修改某字段值的接口,并不复杂。

我们按照Restful的风格来设定,接口信息如下:

  • 接口地址:/admin/questions/{id}/hidden/{hidden}

  • 请求方式:PUT

  • 请求参数: 路径占位符参数

    • id:问题id

    • hidden:是否隐藏

3.7.2.代码实现(练习)

    @Overridepublic void HiddenOrNot(Long id, Boolean hidden) {Long userId = UserContext.getUser();boolean success = lambdaUpdate().eq(InteractionQuestion::getUserId, userId).eq(InteractionQuestion::getId, id).set(InteractionQuestion::getHidden, hidden).update();if(!success){throw new DbException("更新hidden失败");}}

3.8.管理端根据id查询问题详情(练习)

3.8.1.接口分析

在管理端的问题管理页面,点击查看按钮就会进入问题详情页:

问题详情页如下:

可以看到,这里需要查询的数据还是比较多的,包含:

  • 问题标题

  • 问题描述

  • 提问者信息

    • id

    • 昵称

    • 头像

  • 课程三级分类

  • 课程名称

  • 课程负责老师

  • 课程所属章节

  • 回答数量

  • 用户端是否显示

返回值与管理端分页查询基本一致,多了一个课程负责老师信息。所以我们沿用之前的QuestionAdminVO即可。但是需要添加一个课程负责老师的字段:

虽然用户端也有根据id查询问题,但是返回值与用户端存在较大差异,所以我们需要另外设计一个接口。

按照Restful风格,接口信息如下:

  • 接口地址: /admin/questions/{id}

  • 请求方式: GET

  • 请求参数: 路径占位符格式

  • 返回值:与分页查询共享VO,这里不再赘述

3.8.2.代码实现(练习)

    @Overridepublic QuestionAdminVO selectInfoByid(Long id) {InteractionQuestion interactionQuestion = lambdaQuery().eq(InteractionQuestion::getId, id).one();QuestionAdminVO questionAdminVO = BeanUtils.copyBean(interactionQuestion, QuestionAdminVO.class);//查询提问人的id,昵称,头像UserDTO userDTO = userClient.queryUserById(interactionQuestion.getUserId());questionAdminVO.setUserName(userDTO.getName());questionAdminVO.setIcon(userDTO.getIcon());questionAdminVO.setUserId(interactionQuestion.getUserId());//查询课程名称CourseFullInfoDTO course = courseClient.getCourseInfoById(interactionQuestion.getCourseId(), false, false);questionAdminVO.setCourseName(course.getName());//查询老师的 nameList<Long> teacherIds = course.getTeacherIds();UserDTO userDTO1 = userClient.queryUserById(teacherIds.get(0));questionAdminVO.setTeacherName(userDTO1.getName());//查询三级分类questionAdminVO.setCategoryName(categoryCache.getCategoryNames(course.getCategoryIds()));//将该问题的状态标记为 已查看lambdaUpdate().eq(InteractionQuestion::getId, id).set(InteractionQuestion::getStatus, QuestionStatus.CHECKED.getValue()).update();return questionAdminVO;}

4.评论相关接口(练习)

评论相关接口有四个:

  • 新增回答或评论

  • 分页查询回答或评论

  • 管理端分页查询回答或评论

  • 管理端隐藏或显示回答或评论

4.1.新增回答或评论(练习)

4.1.1.接口分析

先来看下回答或评论的表单原型图:

回复本身只有一个简单属性:

  • 回复内容

一个功能属性:

  • 是否匿名

一个关联属性:

  • 问题id:回答要关联某个问题

如果是针对某个回答发表的评论,则有新的关联属性:

  • 回答id:评论是在哪个回答下面的

  • 目标评论id:当前评论是针对哪一条评论的

  • 目标用户id:当前评论是针对哪一个用户的

如果是回答,则只需要前3个属性即可。如果是评论,则还需要补充最后的3个属性。

由于我们把会回答和评论接口合并,因此以上属性都应该作为表单参数。

需要注意的是,在新增回答或评论时,除了要把数据写入interaction_reply表,还有几件事情要做:

  • 判断当前提交的是否是回答,如果是需要在interaction_question中记录最新一次回答的id

  • 判断提交评论的用户是否是学生,如果是标记问题状态为未查看

因此我们的业务流程应该是这样的:

为了方便判断是否是学生提交的回答,我们可以在前端传递一个参数:

  • isStudent:标记当前回答是否是学生提交的

综上,按照Restful的规范设计,接口信息如下:

4.1.2.实体类

按照前面的接口分析,首先定义请求参数DTO,在课前资料中已经提供了:

4.1.3.代码实现

    @Override@Transactionalpublic void saveReply(ReplyDTO replyDTO) {Long userId = UserContext.getUser();//1:判断是回复还是评论if(replyDTO.getAnswerId() == null){ // 当前是回复,不是评论//保存这个回复的数据InteractionReply reply = BeanUtils.copyBean(replyDTO, InteractionReply.class);reply.setCreateTime(LocalDateTime.now());reply.setUpdateTime(LocalDateTime.now());reply.setUserId(userId);save(reply);//查询这个回复的问题,并更新这个问题的数据InteractionQuestion question = questionMapper.selectById(replyDTO.getQuestionId());//更新这个问题最新的回答idif(question != null){question.setLatestAnswerId(reply.getId()); // 更新最新回复时间questionMapper.updateById(question); // 保存更新//判断是否是学生回复,如果是学生回复,就标记该问题为 未查看,并将该问题下的回答数量+1if(replyDTO.getIsStudent()){question.setStatus(QuestionStatus.UN_CHECK);question.setAnswerTimes(question.getAnswerTimes() + 1); // 回答数量+1questionMapper.updateById(question);}}}else{ // 是对回复的评论boolean success = lambdaUpdate().setSql("reply_times = reply_times + 1").eq(InteractionReply::getId, replyDTO.getAnswerId()).update();if(!success){throw new DbException("更新Reply表失败");}//查询这个回复的问题,并更新这个问题的数据InteractionQuestion question = questionMapper.selectById(replyDTO.getQuestionId());//判断是否是学生回复,如果是学生回复,就标记该问题为 未查看,并将该问题下的回答数量+1if(replyDTO.getIsStudent()){question.setStatus(QuestionStatus.UN_CHECK);question.setAnswerTimes(question.getAnswerTimes() + 1); // 回答数量+1questionMapper.updateById(question);}}}

4.2.分页查询回答或评论列表(练习)

4.2.1.接口分析

在问题详情页,除了展示问题详情外,最重要的就是回答列表了,原型图如下:

我们先来分析回答列表,需要展示的内容包括:

  • 回答id

  • 回答内容

  • 是否匿名

  • 回答人信息(如果是匿名,则无需返回)

    • id

    • 昵称

    • 头像

  • 回答时间

  • 评论数量

  • 点赞数量

请求参数就是问题的id。不过需要注意的是,一个问题下的回答比较多,所以一次只能展示一部分,更多数据会采用滚动懒加载模式。简单来说说就是分页查询,所以也要带上分页参数。

再来看一下回答下的评论列表:

仔细观察后可以发现,需要展示的数据与回答及其相似,都包括:

  • 评论id

  • 评论内容

  • 是否匿名

  • 评论人信息(如果是匿名,则无需返回)

    • id

    • 昵称

    • 头像

  • 回答时间

  • 评论数量(无)

  • 点赞数量

  • 目标用户昵称(评论特有)

从返回结果来看:相比回答列表,评论无需展示评论下的评论数量,但是需要展示目标用户的昵称,因为评论是针对某个目标的。

从查询参数来看:查询评论需要知道回答的id,这点与查询回答列表不太一样。

综上,按照Restful的规范设计,接口信息如下:

4.2.2.实体类

请求入参,是一个Query实体,在课前资料的query目录下:

返回值,是一个VO实体,在课前资料的vo目录下:

4.2.3.代码实现

    @Overridepublic PageDTO<ReplyVO> queryReplyPage(ReplyPageQuery query, boolean forAdmin) {// 1.问题id和回答id至少要有一个,先做参数判断Long questionId = query.getQuestionId();Long answerId = query.getAnswerId();if (questionId == null && answerId == null) {throw new BadRequestException("问题或回答id不能都为空");}// 标记当前是查询问题下的回答boolean isQueryAnswer = questionId != null;// 2.分页查询replyPage<InteractionReply> page = lambdaQuery().eq(isQueryAnswer, InteractionReply::getQuestionId, questionId).eq(InteractionReply::getAnswerId, isQueryAnswer ? 0L : answerId).eq(!forAdmin, InteractionReply::getHidden, false).page(query.toMpPage( // 先根据点赞数排序,点赞数相同,再按照创建时间排序new OrderItem(DATA_FIELD_NAME_LIKED_TIME, false),new OrderItem(DATA_FIELD_NAME_CREATE_TIME, true)));List<InteractionReply> records = page.getRecords();if (CollUtils.isEmpty(records)) {return PageDTO.empty(page);}// 3.数据处理,需要查询:提问者信息、回复目标信息、当前用户是否点赞Set<Long> userIds = new HashSet<>();Set<Long> answerIds = new HashSet<>();Set<Long> targetReplyIds = new HashSet<>();// 3.1.获取提问者id 、回复的目标id、当前回答或评论id(统计点赞信息)for (InteractionReply r : records) {if(!r.getAnonymity() || forAdmin) {// 非匿名userIds.add(r.getUserId());}targetReplyIds.add(r.getTargetReplyId());answerIds.add(r.getId());}// 3.2.查询目标回复,如果目标回复不是匿名,则需要查询出目标回复的用户信息targetReplyIds.remove(0L);targetReplyIds.remove(null);if(targetReplyIds.size() > 0) {List<InteractionReply> targetReplies = listByIds(targetReplyIds);Set<Long> targetUserIds = targetReplies.stream().filter(Predicate.not(InteractionReply::getAnonymity).or(r -> forAdmin)).map(InteractionReply::getUserId).collect(Collectors.toSet());userIds.addAll(targetUserIds);}// 3.3.查询用户Map<Long, UserDTO> userMap = new HashMap<>(userIds.size());if(userIds.size() > 0) {List<UserDTO> users = userClient.queryUserByIds(userIds);userMap = users.stream().collect(Collectors.toMap(UserDTO::getId, u -> u));}// 3.4.查询用户点赞状态Set<Long> bizLiked = remarkClient.isBizLiked(answerIds);// 4.处理VOList<ReplyVO> list = new ArrayList<>(records.size());for (InteractionReply r : records) {// 4.1.拷贝基础属性ReplyVO v = BeanUtils.toBean(r, ReplyVO.class);list.add(v);// 4.2.回复人信息if(!r.getAnonymity() || forAdmin){UserDTO userDTO = userMap.get(r.getUserId());if (userDTO != null) {v.setUserIcon(userDTO.getIcon());v.setUserName(userDTO.getName());v.setUserType(userDTO.getType());}}// 4.3.如果存在评论的目标,则需要设置目标用户信息if(r.getTargetReplyId() != null){UserDTO targetUser = userMap.get(r.getTargetUserId());if (targetUser != null) {v.setTargetUserName(targetUser.getName());}}// 4.4.点赞状态v.setLiked(bizLiked.contains(r.getId()));}return new PageDTO<>(page.getTotal(), page.getPages(), list);}

http://www.dtcms.com/a/459158.html

相关文章:

  • 永久免费的wap建站平台泉州优化公司
  • 设计网站的一般过程网站入侵怎么做
  • 网站统计排名WordPress批量修改文章
  • 红色大气网络公司企业网站源码_适合广告设计用divid做网站代码
  • 湖南建设厅网站证书查询做快三网站
  • 陕西交通建设集团信息网站做翻译兼职的网站是哪个
  • 网站建设丷金手指专业十五网上智慧团建官网
  • 网站建设 软件有哪些方面wordpress文章内多页面
  • 南京网站销售网上注册公司的网址
  • 简单的seo网站优化排名aspnet网站开发
  • 崇明建设小学网站福建省鑫通建设有限公司网站
  • 锐途网站建设写作网站投稿赚钱
  • 做个电商网站和app网站禁止右键
  • 浙江建设厅网站首页新企业名录数据免费
  • wordpress 自动链接seo项目
  • 如何逐步提升网站权重哪里有网站建设服务
  • 大型门户网站建设服务网站开发商外包
  • 学校网站建设的意义和应用石家庄软件开发培训学校
  • 归并排序:高效稳定分治之道
  • 网站缓存设置怎么做太原app制作
  • 郑州专业建站报价有深度网站
  • 以太网数据报文字段全解析:从物理层到应用层的协议交响曲
  • 怎样保证网站的安全wordpress免费绑定域名
  • wordpress小型论坛主题网站建设优化推广哈尔滨
  • wordpress微信主页外贸建站seo优化
  • 专业网站设计开发热烈祝贺网站上线
  • 公共安全事件分析-5_文章思路
  • 建设网站几钱河北邯郸大风
  • PN结的交流等效电阻
  • 外贸做的社交网站网站开发多长时间