Community 论坛项目

转载请附带原文链接:

1. 环境搭建与技术栈说明

1.0 项目架构图

1.1 技术要求

  • 熟悉快速开发框架:SpringBoot2.3.x 整合 SpringMVC + Mybatis
  • 熟悉版本控制:Maven3.6.X + Git
  • 数据库以及文件存储:MySQL + 文件存储阿里云OSS
  • 熟悉页面模板引擎:Thymleaf3.x
  • 第三方工具:网页长图生成工具Wkhtmltopdf + 验证码生成工具kaptcha
  • 中间件:分布式缓存Redis + 全文检索ElasticSearch + Kafka + 本地缓存Caffeine
  • 权限框架:Spring Securtiy + Spring Actuator
  • 熟悉前端:Ajax + Vue + BootStrap + HTML + jQuery

1.2 环境搭建

初始化SpringBoot项目:

初始化后的pom.xml:

<!--thymleaf-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--web-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis-->
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.4</version>
</dependency>
<!--热部署-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional>
</dependency>
<!--mysql-->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope>
</dependency>
<!--lombok-->
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional>
</dependency>
<!--test-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions>
</dependency>

项目初始结构:

1.3 数据库设计

数据库表sql

SET NAMES utf8 ;
--
-- Table structure for table `comment`
--
DROP TABLE IF EXISTS `comment`;SET character_set_client = utf8mb4 ;
CREATE TABLE `comment` (`id` int(11) NOT NULL AUTO_INCREMENT,`user_id` int(11) DEFAULT NULL,`entity_type` int(11) DEFAULT NULL,`entity_id` int(11) DEFAULT NULL,`target_id` int(11) DEFAULT NULL,`content` text,`status` int(11) DEFAULT NULL,`create_time` timestamp NULL DEFAULT NULL,PRIMARY KEY (`id`),KEY `index_user_id` (`user_id`) /*!80000 INVISIBLE */,KEY `index_entity_id` (`entity_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--
-- Table structure for table `discuss_post`
--
DROP TABLE IF EXISTS `discuss_post`;SET character_set_client = utf8mb4 ;
CREATE TABLE `discuss_post` (`id` int(11) NOT NULL AUTO_INCREMENT,`user_id` varchar(45) DEFAULT NULL,`title` varchar(100) DEFAULT NULL,`content` text,`type` int(11) DEFAULT NULL COMMENT '0-普通; 1-置顶;',`status` int(11) DEFAULT NULL COMMENT '0-正常; 1-精华; 2-拉黑;',`create_time` timestamp NULL DEFAULT NULL,`comment_count` int(11) DEFAULT NULL,`score` double DEFAULT NULL,PRIMARY KEY (`id`),KEY `index_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--
-- Table structure for table `login_ticket`
--
DROP TABLE IF EXISTS `login_ticket`;SET character_set_client = utf8mb4 ;
CREATE TABLE `login_ticket` (`id` int(11) NOT NULL AUTO_INCREMENT,`user_id` int(11) NOT NULL,`ticket` varchar(45) NOT NULL,`status` int(11) DEFAULT '0' COMMENT '0-有效; 1-无效;',`expired` timestamp NOT NULL,PRIMARY KEY (`id`),KEY `index_ticket` (`ticket`(20))
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--
-- Table structure for table `message`
--
DROP TABLE IF EXISTS `message`;SET character_set_client = utf8mb4 ;
CREATE TABLE `message` (`id` int(11) NOT NULL AUTO_INCREMENT,`from_id` int(11) DEFAULT NULL,`to_id` int(11) DEFAULT NULL,`conversation_id` varchar(45) NOT NULL,`content` text,`status` int(11) DEFAULT NULL COMMENT '0-未读;1-已读;2-删除;',`create_time` timestamp NULL DEFAULT NULL,PRIMARY KEY (`id`),KEY `index_from_id` (`from_id`),KEY `index_to_id` (`to_id`),KEY `index_conversation_id` (`conversation_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--
-- Table structure for table `user`
--
DROP TABLE IF EXISTS `user`;SET character_set_client = utf8mb4 ;
CREATE TABLE `user` (`id` int(11) NOT NULL AUTO_INCREMENT,`username` varchar(50) DEFAULT NULL,`password` varchar(50) DEFAULT NULL,`salt` varchar(50) DEFAULT NULL,`email` varchar(100) DEFAULT NULL,`type` int(11) DEFAULT NULL COMMENT '0-普通用户; 1-超级管理员; 2-版主;',`status` int(11) DEFAULT NULL COMMENT '0-未激活; 1-已激活;',`activation_code` varchar(100) DEFAULT NULL,`header_url` varchar(200) DEFAULT NULL,`create_time` timestamp NULL DEFAULT NULL,PRIMARY KEY (`id`),KEY `index_username` (`username`(20)),KEY `index_email` (`email`(20))
) ENGINE=InnoDB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8;

之后会提供一些

2. 邮件发送功能

2.1 发送者邮箱中打开SMTP服务

首先在自己的邮箱(网易、QQ…均可)设置中开启SMTP服务

2.2 引入依赖

pom.xml中引入依赖

 <!--引入邮件发送依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-mail</artifactId></dependency>

2.3 参数配置

邮箱参数配置(我使用的是网易邮箱)

# spring 相关配置
spring:# 发送者邮箱相关配置mail:# SMTP服务器域名host: smtp.163.com# 编码集default-encoding: UTF-8# 邮箱用户名username: csp******@163.com# 授权码(注意不是邮箱密码!)password: WDS*******XCQA# 协议:smtpsprotocol: smtps# 详细配置properties:mail:smtp:# 设置是否需要认证,如果为true,那么用户名和密码就必须的,# 如果设置false,可以不设置用户名和密码# (前提要知道对接的平台是否支持无密码进行访问的)auth: true# STARTTLS[1]  是对纯文本通信协议的扩展。# 它提供一种方式将纯文本连接升级为加密连接(TLS或SSL)# 而不是另外使用一个端口作加密通信。starttls:enable: truerequired: true

2.4 邮件发送工具类

/*** @Auther: csp1999* @Date: 2020/11/24/14:29* @Description: 邮件发送客户端*/
@Component
public class MailClient {private static final Logger logger = LoggerFactory.getLogger(MailClient.class);@Autowiredprivate JavaMailSender mailSender;@Value("${spring.mail.username}")private String from;/*** 发送邮件* @param to 收件人* @param subject 邮件主题* @param content 邮件内容*/public void sendMail(String to,String subject,String content){try {MimeMessage message = mailSender.createMimeMessage();MimeMessageHelper helper = new MimeMessageHelper(message);helper.setFrom(from);// 发送者helper.setTo(to);// 接收者helper.setSubject(subject);// 邮件主题helper.setText(content,true);// 邮件内容,第二个参数true表示支持html格式mailSender.send(helper.getMimeMessage());} catch (MessagingException e) {logger.error("发送邮件失败: " + e.getMessage());}}
}

2.5 测试发送

@Autowired
private MailClient mailClient;@Test
void test02(){mailClient.sendMail("11xxxxxxx@qq.com","TEST","测试邮件发送!");
}

测试发送邮件成功!

2.6 使用Thymleaf模板引擎发送html格式的邮件

...    // 激活邮件发送Context context = new Context();// org.thymeleaf.context.Context 包下context.setVariable("email", user.getEmail());// http://csp1999.natapp1.cc/community/activation/用户id/激活码String url = path + contextPath + "/activation/" + user.getId() + "/" + user.getActivatiocontext.setVariable("url", url);String content = templateEngine.process("/mail/activation", context);mailClient.sendMail(user.getEmail(), "激活账号", content);
...

3. 登录与注册功能

  • 登录注册功能的验证码目前是存放在Session中,之后要存入Redis,提高性能,同时也可以解决分布式部署时的Session共享问题!
  • 注册功能的邮件发送,比较费时,用户只能干等待邮件发送成功,这种方式不太友好,因此在后端以多线程的方式,分一个线程去处理邮件发送,进而不影响客户端正常给用户的响应问题,不用让用户在页面卡太长时间!
  • 对于登录用户信息判定(比如,账号密码是否错误,用户名是否存在,用户是否激活)等问题,如果每次都查询数据库,效率比较低,为此我们在客户端发送请求——>后端调用数据库,之间加一层 Redis 缓存,来验证用户登录信息是否合法!

3.1 登录功能

3.2 注册功能

4.通过cookie获取user登录信息

客户端通过cookie携带登录凭证向服务器换取user信息,流程如图:

这一流程需要借助拦截器LoginTicketInterceptor 和 LoginRequiredInterceptor实现!

LoginTicketInterceptor.java 登录凭证拦截器

/*** @Auther: csp1999* @Date: 2020/11/24/20:54* @Description: 登录凭证拦截器*/
@Component
public class LoginTicketInterceptor implements HandlerInterceptor {@Autowiredprivate UserService userService;@Autowiredprivate HostHolder hostHolder;/*** 请求开始前* @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request,HttpServletResponse response, Object handler) throws Exception {// 从cookie中获取凭证String ticket = CookieUtil.getValue(request, "ticket");if (ticket != null) {// 查询凭证LoginTicket loginTicket = userService.findLoginTicket(ticket);// 检查凭证是否有效if (loginTicket != null && loginTicket.getStatus() == 0&& loginTicket.getExpired().after(new Date())) {// 根据凭证查询用户User user = userService.findUserById(loginTicket.getUserId());// 在本次请求中(当前线程)持有该用户信息(要考虑多线程并发的情况,所以借助ThreadLocal)hostHolder.setUser(user);}}return true;}/*** 执行请求时* @param request* @param response* @param handler* @param modelAndView* @throws Exception*/@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {// 从ThreadLocal 中得到当前线程持有的userUser user = hostHolder.getUser();if (user != null && modelAndView != null) {// 登录用户的信息存入modelAndViewmodelAndView.addObject("loginUser", user);}}/*** 请求结束后* @param request* @param response* @param handler* @param ex* @throws Exception*/@Overridepublic void afterCompletion(HttpServletRequest request,HttpServletResponse response, Object handler, Exception ex) throws Exception {// 从ThreadLocal清除数据hostHolder.clear();}
}

LoginRequiredInterceptor.java 登录请求拦截器

/*** @Auther: csp1999* @Date: 2020/11/24/21:27* @Description: 登录请求拦截器*/
@Component
public class LoginRequiredInterceptor implements HandlerInterceptor {@Autowiredprivate HostHolder hostHolder;/*** 请求开始前** @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 判断handler 是否是 HandlerMethod 类型if (handler instanceof HandlerMethod) {HandlerMethod handlerMethod = (HandlerMethod) handler;// 获取到方法实例Method method = handlerMethod.getMethod();// 从方法实例中获得其 LoginRequired 注解LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);// 如果方法实例上标注有 LoginRequired 注解,但 hostHandler中没有 用户信息则拦截if (loginRequired != null && hostHolder.getUser() == null) {response.sendRedirect(request.getContextPath() + "/login");return false;}}return true;}

将拦截器注册到spring容器中

/*** @Auther: csp1999* @Date: 2020/11/24/20:53* @Description: 拦截器配置类*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {@Autowiredprivate LoginTicketInterceptor loginTicketInterceptor;@Autowiredprivate LoginRequiredInterceptor loginRequiredInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginTicketInterceptor)// 除了静态资源不拦截,其他都拦截.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");registry.addInterceptor(loginRequiredInterceptor)// 除了静态资源不拦截,其他都拦截.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");}
}

5. 文件/头像上传服务器

5.1 效果展示

上传头像:

头像上传成功:

5.2 阿里云OSS文件存储

入门参考文章:springboot操作阿里云OSS实现文件上传,下载,删除(附源码)

AliyunOssConfig

/*** @Auther: csp1999* @Date: 2020/10/31/13:33* @Description: 阿里云 OSS 基本配置*/
// 声明配置类,放入Spring容器
@Configuration
// 指定配置文件位置
@PropertySource(value = {"classpath:application-aliyun-oss.properties"})
// 指定配置文件中自定义属性前缀
@ConfigurationProperties(prefix = "aliyun")
@Data// lombok
@Accessors(chain = true)// 开启链式调用
public class AliyunOssConfig {private String endPoint;// 地域节点private String accessKeyId;private String accessKeySecret;private String bucketName;// OSS的Bucket名称private String urlPrefix;// Bucket 域名private String fileHost;// 目标文件夹// 将OSS 客户端交给Spring容器托管@Beanpublic OSS OSSClient() {return new OSSClient(endPoint, accessKeyId, accessKeySecret);}
}

FileUploadService

/*** @Auther: csp1999* @Date: 2020/10/31/14:30* @Description: 文件上传Service (为节省文章中的代码篇幅,不再做接口实现类处理)*/
@Service("fileUploadService")
public class FileUploadService {// 允许上传文件(图片)的格式private static final String[] IMAGE_TYPE = new String[]{".bmp", ".jpg",".jpeg", ".gif", ".png"};private static final Logger logger = LoggerFactory.getLogger(FileUploadService.class);@Autowiredprivate OSS ossClient;// 注入阿里云oss文件服务器客户端@Autowiredprivate AliyunOssConfig aliyunOssConfig;// 注入阿里云OSS基本配置类/*** 文件上传* 注:阿里云OSS文件上传官方文档链接:https://help.aliyun.com/document_detail/84781.html?spm=a2c4g.11186623.6.749.11987a7dRYVSzn** @param: uploadFile* @return: string* @create: 2020/10/31 14:36* @author: csp1999*/public String upload(MultipartFile uploadFile) {// 获取oss的Bucket名称String bucketName = aliyunOssConfig.getBucketName();// 获取oss的地域节点String endpoint = aliyunOssConfig.getEndPoint();// 获取oss的AccessKeySecretString accessKeySecret = aliyunOssConfig.getAccessKeySecret();// 获取oss的AccessKeyIdString accessKeyId = aliyunOssConfig.getAccessKeyId();// 获取oss目标文件夹String filehost = aliyunOssConfig.getFileHost();// 返回图片上传后返回的urlString returnImgeUrl = "";// 校验图片格式boolean isLegal = false;for (String type : IMAGE_TYPE) {if (StringUtils.endsWithIgnoreCase(uploadFile.getOriginalFilename(), type)) {isLegal = true;break;}}if (!isLegal) {// 如果图片格式不合法logger.info("图片格式不符合要求...");}// 获取文件原名称String originalFilename = uploadFile.getOriginalFilename();// 获取文件类型String fileType = originalFilename.substring(originalFilename.lastIndexOf("."));// 新文件名称String newFileName = UUID.randomUUID().toString() + fileType;// 构建日期路径, 例如:OSS目标文件夹/2020/10/31/文件名String filePath = new SimpleDateFormat("yyyy/MM/dd").format(new Date());// 文件上传的路径地址String uploadImgeUrl = filehost + "/" + filePath + "/" + newFileName;// 获取文件输入流InputStream inputStream = null;try {inputStream = uploadFile.getInputStream();} catch (IOException e) {e.printStackTrace();}/*** 下面两行代码是重点坑:* 现在阿里云OSS 默认图片上传ContentType是image/jpeg* 也就是说,获取图片链接后,图片是下载链接,而并非在线浏览链接,* 因此,这里在上传的时候要解决ContentType的问题,将其改为image/jpg*/ObjectMetadata meta = new ObjectMetadata();meta.setContentType("image/jpg");//文件上传至阿里云OSSossClient.putObject(bucketName, uploadImgeUrl, inputStream, meta);/*** 注意:在实际项目中,文件上传成功后,数据库中存储文件地址*/// 获取文件上传后的图片返回地址returnImgeUrl = "http://" + bucketName + "." + endpoint + "/" + uploadImgeUrl;return returnImgeUrl;}
}

6. 敏感词过滤

使用前缀树的数据结构,来进行敏感词过滤:

  • 第一步:在resource 目录下新建 sensitive-words.txt 敏感词文本文件
  • 第二步:新建一个敏感词过滤组件 SensitiveFilter
/*** @Auther: csp1999* @Date: 2020/11/25/10:56* @Description: 敏感词过滤组件*/
@Component
public class SensitiveFilter {private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);// 替换符private static final String REPLACEMENT = "***";// 根节点private TrieNode rootNode = new TrieNode();@PostConstructpublic void init() {try (InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");BufferedReader reader = new BufferedReader(new InputStreamReader(is));) {String keyword;while ((keyword = reader.readLine()) != null) {// 添加到前缀树this.addKeyword(keyword);}} catch (IOException e) {logger.error("加载敏感词文件失败: " + e.getMessage());}}// 将一个敏感词添加到前缀树中private void addKeyword(String keyword) {TrieNode tempNode = rootNode;for (int i = 0; i < keyword.length(); i++) {char c = keyword.charAt(i);TrieNode subNode = tempNode.getSubNode(c);if (subNode == null) {// 初始化子节点subNode = new TrieNode();tempNode.addSubNode(c, subNode);}// 指向子节点,进入下一轮循环tempNode = subNode;// 设置结束标识if (i == keyword.length() - 1) {tempNode.setKeywordEnd(true);}}}/*** 过滤敏感词** @param text 待过滤的文本* @return 过滤后的文本*/public String filter(String text) {if (StringUtils.isBlank(text)) {return null;}// 指针1TrieNode tempNode = rootNode;// 指针2int begin = 0;// 指针3int position = 0;// 结果StringBuilder sb = new StringBuilder();while (position < text.length()) {char c = text.charAt(position);// 跳过符号if (isSymbol(c)) {// 若指针1处于根节点,将此符号计入结果,让指针2向下走一步if (tempNode == rootNode) {sb.append(c);begin++;}// 无论符号在开头或中间,指针3都向下走一步position++;continue;}// 检查下级节点tempNode = tempNode.getSubNode(c);if (tempNode == null) {// 以begin开头的字符串不是敏感词sb.append(text.charAt(begin));// 进入下一个位置position = ++begin;// 重新指向根节点tempNode = rootNode;} else if (tempNode.isKeywordEnd()) {// 发现敏感词,将begin~position字符串替换掉sb.append(REPLACEMENT);// 进入下一个位置begin = ++position;// 重新指向根节点tempNode = rootNode;} else {// 检查下一个字符position++;}}// 将最后一批字符计入结果sb.append(text.substring(begin));return sb.toString();}// 判断是否为符号private boolean isSymbol(Character c) {// 0x2E80~0x9FFF 是东亚文字范围return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);}// 前缀树private class TrieNode {// 关键词结束标识private boolean isKeywordEnd = false;// 子节点(key是下级字符,value是下级节点)private Map<Character, TrieNode> subNodes = new HashMap<>();public boolean isKeywordEnd() {return isKeywordEnd;}public void setKeywordEnd(boolean keywordEnd) {isKeywordEnd = keywordEnd;}// 添加子节点public void addSubNode(Character c, TrieNode node) {subNodes.put(c, node);}// 获取子节点public TrieNode getSubNode(Character c) {return subNodes.get(c);}}
}

效果如下图:

7.帖子发布与帖子评论

7.1 帖子发布

7.2 帖子评论

8. 私信列表与私信会话聊天

8.1 效果如图

私信列表

私信详情

私信发送

8.2 DAO层代码

Mapper接口

/*** @Auther: csp1999* @Date: 2020/11/26/16:29* @Description:*/
@Repository
public interface MessageMapper {/*** 查询当前用户的会话列表,针对每个会话只返回一条最新的私信.* @param userId* @param offset* @param limit* @return*/List<Message> selectConversations(@Param("userId") int userId,@Param("offset")int offset,@Param("limit") int limit);/*** 查询当前用户的会话数量.* @param userId* @return*/int selectConversationCount(@Param("userId")int userId);/*** 查询某个会话所包含的私信列表.* @param conversationId* @param offset* @param limit* @return*/List<Message> selectLetters(@Param("conversationId")String conversationId,@Param("offset")int offset,@Param("limit")int limit);/*** 查询某个会话所包含的私信数量.* @param conversationId* @return*/int selectLetterCount(@Param("conversationId")String conversationId);/*** 查询未读私信的数量* @param userId* @param conversationId* @return*/int selectLetterUnreadCount(@Param("userId")int userId,@Param("conversationId")String conversationId);/*** 新增消息* @param message* @return*/int insertMessage(Message message);/*** 修改消息的状态* @param ids* @param status* @return*/int updateStatus(@Param("ids")List<Integer> ids,@Param("status")int status);
}

SQL实现

考验sql能力的时候到了(∩_∩)!

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.haust.community.mapper.MessageMapper"><sql id="selectFields">id, from_id, to_id, conversation_id, content, status, create_time</sql><sql id="insertFields">from_id, to_id, conversation_id, content, status, create_time</sql><!-- 查询当前用户的会话列表,针对每个会话只返回一条最新的私信. --><select id="selectConversations" resultType="com.haust.community.pojo.Message">select<include refid="selectFields"></include>from messagewhere id in (select max(id) from messagewhere status != 2and from_id != 1and (from_id = #{userId} or to_id = #{userId})group by conversation_id)order by id desclimit #{offset}, #{limit}</select><!-- 查询当前用户的会话数量. --><select id="selectConversationCount" resultType="java.lang.Integer">select count(m.maxid) from (select max(id) as maxid from messagewhere status != 2and from_id != 1and (from_id = #{userId} or to_id = #{userId})group by conversation_id) as m</select><!-- 询某个会话所包含的私信列表. --><select id="selectLetters" resultType="com.haust.community.pojo.Message">select<include refid="selectFields"></include>from messagewhere status != 2and from_id != 1and conversation_id = #{conversationId}order by id desclimit #{offset}, #{limit}</select><!-- 查询某个会话所包含的私信数量. --><select id="selectLetterCount" resultType="java.lang.Integer">select count(id)from messagewhere status != 2and from_id != 1and conversation_id = #{conversationId}</select><!-- 查询未读私信的数量. --><select id="selectLetterUnreadCount" resultType="java.lang.Integer">select count(id)from messagewhere status = 0and from_id != 1and to_id = #{userId}<if test="conversationId!=null">and conversation_id = #{conversationId}</if></select><!-- 新增消息. --><insert id="insertMessage" parameterType="com.haust.community.pojo.Message" keyProperty="id">insert into message(<include refid="insertFields"></include>)values(#{fromId},#{toId},#{conversationId},#{content},#{status},#{createTime})</insert><!-- 修改消息的状态. --><update id="updateStatus">update message set status = #{status}where id in<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach></update>
</mapper>

Controller API

/*** @Auther: csp1999* @Date: 2020/11/26/17:42* @Description:*/
@Controller
public class MessageController {@Autowiredprivate MessageService messageService;@Autowiredprivate HostHolder hostHolder;@Autowiredprivate UserService userService;/*** 获取用户私信列表(支持分页) api** @param model* @param page* @return*/@RequestMapping(path = "/letter/list", method = RequestMethod.GET)public String getLetterList(Model model, Page page) {User user = hostHolder.getUser();// 分页信息page.setLimit(5);page.setPath("/letter/list");page.setRows(messageService.findConversationCount(user.getId()));// 会话列表List<Message> conversationList = messageService.findConversations(user.getId(), page.getOffset(), page.getLimit());List<Map<String, Object>> conversations = new ArrayList<>();if (conversationList != null) {for (Message message : conversationList) {Map<String, Object> map = new HashMap<>();// 会话map.put("conversation", message);// 会话中的消息数量map.put("letterCount", messageService.findLetterCount(message.getConversationId()));// 会话中的未读消息数量map.put("unreadCount", messageService.findLetterUnreadCount(user.getId(),message.getConversationId()));int targetId = user.getId() == message.getFromId() ? message.getToId() : message.getFromId();// 目标id(消息接收者id)map.put("target", userService.findUserById(targetId));// 该会话加入会话列表conversations.add(map);}}// 会话列表加入model中model.addAttribute("conversations", conversations);// 查询未读消息数量int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(), null);// 未读消息数量加入model中model.addAttribute("letterUnreadCount", letterUnreadCount);return "/site/letter";}/*** 私信详情 api* @param conversationId* @param page* @param model* @return*/@RequestMapping(path = "/letter/detail/{conversationId}", method = RequestMethod.GET)public String getLetterDetail(@PathVariable("conversationId") String conversationId, Page page, Model model) {// 分页信息page.setLimit(5);page.setPath("/letter/detail/" + conversationId);page.setRows(messageService.findLetterCount(conversationId));// 私信列表List<Message> letterList = messageService.findLetters(conversationId, page.getOffset(), page.getLimit());List<Map<String, Object>> letters = new ArrayList<>();if (letterList != null) {for (Message message : letterList) {Map<String, Object> map = new HashMap<>();// 会话消息map.put("letter", message);// 消息发送者信息map.put("fromUser", userService.findUserById(message.getFromId()));letters.add(map);}}// 会话消息列表存入modelmodel.addAttribute("letters", letters);// 私信目标存入modelmodel.addAttribute("target", getLetterTarget(conversationId));// 设置已读List<Integer> ids = getLetterIds(letterList);if (!ids.isEmpty()) {messageService.readMessage(ids);}return "/site/letter-detail";}// 获取私信目标信息private User getLetterTarget(String conversationId) {// 分割conversationId  eg: 111_112  ---> [111,222]String[] ids = conversationId.split("_");int id0 = Integer.parseInt(ids[0]);int id1 = Integer.parseInt(ids[1]);if (hostHolder.getUser().getId() == id0) {return userService.findUserById(id1);} else {return userService.findUserById(id0);}}// 根据会话消息id集合批量签收(读取)多条消息private List<Integer> getLetterIds(List<Message> letterList) {List<Integer> ids = new ArrayList<>();if (letterList != null) {for (Message message : letterList) {if (hostHolder.getUser().getId() == message.getToId() && message.getStatus() == 0) {ids.add(message.getId());}}}return ids;}/*** 私信发送操作* @param toName* @param content* @return*/@RequestMapping(path = "/letter/send", method = RequestMethod.POST)@ResponseBodypublic String sendLetter(String toName, String content) {User target = userService.findUserByName(toName);if (target == null) {return CommunityUtil.getJSONString(1, "目标用户不存在!");}// 开始构建会话消息对象Message message = new Message();message.setFromId(hostHolder.getUser().getId());message.setToId(target.getId());if (message.getFromId() < message.getToId()) {message.setConversationId(message.getFromId() + "_" + message.getToId());} else {message.setConversationId(message.getToId() + "_" + message.getFromId());}message.setContent(content);message.setCreateTime(new Date());messageService.addMessage(message);return CommunityUtil.getJSONString(0);}
}

9. 全局异常捕获与处理

404页面展示

错误页面展示

统一异常处理

相关注解介绍