三、开发社区核心功能
3.1、过滤敏感词(前缀树)
什么 是前缀树?
参考LeetCode
208. 实现 Trie (前缀树)
-
在resource下定义一个敏感词文件
sensitive-words.txt
-
在
util
工具类中定义敏感词过滤工具类SensitiveFilter.java
:
(1) 定义前缀树 :
// 前缀树
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);
}
// 获取当前节点的指定c子节点
public TrieNode getSubNode(Character c) {
return subNodes.get(c);
}
}
(2)初始化敏感词的前缀树 :
private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);
// 替换符
private static final String REPLACEMENT = "***";
// 根节点
private TrieNode rootNode = new TrieNode();
@PostConstruct
public void init() {
try ( //resource下的classse包下的sensitive-words.txt加载到字节流中
//放在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 私有方法
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) {
//如果不存在c子节点,那么新建一个c子节点
subNode = new TrieNode();
tempNode.addSubNode(c, subNode);
}
// 指向子节点,进入下一轮循环
tempNode = subNode;
// 设置结束标识
if (i == keyword.length() - 1) {
tempNode.setKeywordEnd(true);
}
}
}
(3)方法:过滤敏感词 public 外部可调用
/**
* 过滤敏感词
*
* @param text 待过滤的文本
* @return 过滤后的文本
*/
public String filter(String text) {
if (StringUtils.isBlank(text)) {
return null;
}
// 指针1,指向前缀树
TrieNode tempNode = rootNode;
// 指针2
int begin = 0;
// 指针3
int 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++;
}
}
// begin还没到结尾,position先到结尾的情况:将最后一批字符计入结果
sb.append(text.substring(begin));
return sb.toString();
}
// 判断是否为符号
private boolean isSymbol(Character c) {
// 0x2E80~0x9FFF 是东亚文字范围
return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
}
(4)测试:SensitiveTests.java
3.2、发布帖子(异步请求)
异步请求示例:
- 导入
fastjson
依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
CommunityUtil.java
//code:编码 msg:提示信息 map:封装业务数据
public static String getJSONString(int code, String msg, Map<String, Object> map) {
JSONObject json = new JSONObject();
json.put("code", code);
json.put("msg", msg);
if (map != null) {
for (String key : map.keySet()) {
json.put(key, map.get(key));
}
}
return json.toJSONString();
}
//重载:
public static String getJSONString(int code, String msg) {
return getJSONString(code, msg, null);
}
public static String getJSONString(int code) {
return getJSONString(code, null, null);
}
- controller层
AlphaController.java
:
// ajax示例
@RequestMapping(path = "/ajax", method = RequestMethod.POST)
@ResponseBody
public String testAjax(String name, int age) {
System.out.println(name);
System.out.println(age);
return CommunityUtil.getJSONString(0, "操作成功!");
}
ajax-demo.html
<!-- 引入jquery-->
<script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script>
(1)dao层
DiscussPostMapper
接口中添加:增加帖子的方法
int insertDiscussPost(DiscussPost discussPost);
resource.mapper.discusspost-mapper.xml
<sql id="insertFields">
user_id, title, content, type, status, create_time, comment_count, score
</sql>
<insert id="insertDiscussPost" parameterType="DiscussPost">
insert into discuss_post(<include refid="insertFields"></include>)
values(#{userId},#{title},#{content},#{type},#{status},#{createTime},#{commentCount},#{score})
</insert>
(2)service层
DiscussPostService.java
中增加一个 增加帖子的方法:
@Autowired
private SensitiveFilter sensitiveFilter;
public int addDiscussPost(DiscussPost post) {
if (post == null) {
throw new IllegalArgumentException("参数不能为空!");
}
// 转义HTML标记
post.setTitle(HtmlUtils.htmlEscape(post.getTitle()));
post.setContent(HtmlUtils.htmlEscape(post.getContent()));
// 过滤敏感词
post.setTitle(sensitiveFilter.filter(post.getTitle()));
post.setContent(sensitiveFilter.filter(post.getContent()));
return discussPostMapper.insertDiscussPost(post);
}
(3)Controller层
DiscussPostController:
@Controller
@RequestMapping("/discuss")
public class DiscussPostController implements CommunityConstant {
@Autowired
private DiscussPostService discussPostService;
@Autowired
private HostHolder hostHolder;
@RequestMapping(path = "/add", method = RequestMethod.POST)
@ResponseBody
public String addDiscussPost(String title, String content) {
User user = hostHolder.getUser();
if (user == null) {
return CommunityUtil.getJSONString(403, "你还没有登录哦!");//异步
}
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setTitle(title);
post.setContent(content);
post.setCreateTime(new Date());
discussPostService.addDiscussPost(post);
// 报错的情况,将来统一处理.
return CommunityUtil.getJSONString(0, "发布成功!");
}
}
前端:index.js
实现异步请求
3.3、帖子详情
(1)dao层
DiscussPostMapper
增加 查询帖子详情方法
DiscussPost selectDiscussPostById(int id);
discusspost-mapper.xml:
<select id="selectDiscussPostById" resultType="DiscussPost">
select <include refid="selectFields"></include>
from discuss_post
where id = #{id}
</select>
(2)service层
DiscussPostService.java
public int findDiscussPostRows(int userId) {
return discussPostMapper.selectDiscussPostRows(userId);
}
(3)controller层
DiscussPostController.java
@Autowired
private UserService userService;
@RequestMapping(path = "/detail/{discussPostId}", method = RequestMethod.GET)
public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model) {//从路径中获取用户id
// 帖子
DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
model.addAttribute("post", post);
// 作者
User user = userService.findUserById(post.getUserId());
model.addAttribute("user", user);
return "/site/discuss-detail";
}
index.html
3.4、事务管理
spring事务管理:
-
申明式事务
- 通过xml配置,申明某方法的事务特征
- 通过注解,申明某方法的事务特征:@Transactional(isolation = …, propagation = …)
-
编程式事务
- 通过 Transaction Template 事务管理,并通过它执行数据库操作
- 声明式事务示例:
AlphaService.java:
// REQUIRED: 支持当前事务(外部事务),如果不存在则创建新事务.
// REQUIRES_NEW: 创建一个新事务,并且暂停当前事务(外部事务).
// NESTED: 如果当前存在事务(外部事务),则嵌套在该事务中执行(独立的提交和回滚),否则就会REQUIRED一样.
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public Object save1() {
// 新增用户
User user = new User();
user.setUsername("alpha");
user.setSalt(CommunityUtil.generateUUID().substring(0, 5));
user.setPassword(CommunityUtil.md5("123" + user.getSalt()));
user.setEmail("alpha@qq.com");
user.setHeaderUrl("http://image.nowcoder.com/head/99t.png");
user.setCreateTime(new Date());
userMapper.insertUser(user);
// 新增帖子
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setTitle("Hello");
post.setContent("新人报道!");
post.setCreateTime(new Date());
discussPostMapper.insertDiscussPost(post);
Integer.valueOf("abc");
return "ok";
}
- 编程式事务示例:
public Object save2() {
transactionTemplate.setIsolationLevel(
TransactionDefinition.ISOLATION_READ_COMMITTED);
transactionTemplate.setPropagationBehavior(
TransactionDefinition.PROPAGATION_REQUIRED);
return transactionTemplate.execute(new TransactionCallback<Object>() {
@Override
public Object doInTransaction(TransactionStatus status) {
// 新增用户
User user = new User();
user.setUsername("beta");
user.setSalt(CommunityUtil.generateUUID().substring(0, 5));
user.setPassword(CommunityUtil.md5("123" + user.getSalt()));
user.setEmail("beta@qq.com");
user.setHeaderUrl("http://image.nowcoder.com/head/999t.png");
user.setCreateTime(new Date());
userMapper.insertUser(user);
// 新增帖子
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setTitle("你好");
post.setContent("我是新人!");
post.setCreateTime(new Date());
discussPostMapper.insertDiscussPost(post);
Integer.valueOf("abc");
return "ok";
}
});
}
3.5、显示评论
(1)dao层
数据库表:
CREATE TABLE `comment` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int DEFAULT NULL,#帖子发布者的id
`entity_type` int DEFAULT NULL, #对帖子的评论、对评论的评论、课程的评论....
`entity_id` int DEFAULT NULL, # 具体类型的id,如果评论的是帖子,则对应的是帖子的id
`target_id` int DEFAULT NULL, #记录评论指向哪个人, 如果评论的是回复,这个值才有,否则无
`content` text,
`status` int DEFAULT NULL, #0表示正常 1表示已经被删除
`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 AUTO_INCREMENT=276 DEFAULT CHARSET=utf8
新建实体类:Comment.java
新建mapper:CommentMapper.java
@Mapper
public interface CommentMapper {
List<Comment> selectCommentsByEntity(int entityType, int entityId, int offset, int limit);//根据实体来查询评论
int selectCountByEntity(int entityType, int entityId);//查询评论数
}
新建 comment-mapper.xml
<sql id="selectFields">
id, user_id, entity_type, entity_id, target_id, content, status, create_time
</sql>
<select id="selectCommentsByEntity" resultType="Comment">
select <include refid="selectFields"></include>
from comment
where status = 0
and entity_type = #{entityType}
and entity_id = #{entityId}
order by create_time asc
limit #{offset}, #{limit}
</select>
<select id="selectCountByEntity" resultType="int">
select count(id)
from comment
where status = 0
and entity_type = #{entityType}
and entity_id = #{entityId}
</select>
(2)service层
CommentService.java
@Service
public class CommentService implements CommunityConstant {
@Autowired
private CommentMapper commentMapper;
public List<Comment> findCommentsByEntity(int entityType, int entityId, int offset, int limit) {
return commentMapper.selectCommentsByEntity(entityType, entityId, offset, limit);
}
public int findCommentCount(int entityType, int entityId) {
return commentMapper.selectCountByEntity(entityType, entityId);
}
}
(3)controller层
DiscussPostController
之前的只查询帖子的标题、内容、作者,现在要加入帖子的评论,并带分页
@RequestMapping(path = "/detail/{discussPostId}", method = RequestMethod.GET)
public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model, Page page) {
// 帖子
DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
model.addAttribute("post", post);
// 作者
User user = userService.findUserById(post.getUserId());
model.addAttribute("user", user);
// 评论分页信息
page.setLimit(5);//每页显示5条
page.setPath("/discuss/detail/" + discussPostId);//路径
page.setRows(post.getCommentCount());
// 评论: 给帖子的评论
// 回复: 给评论的评论
// 评论列表
List<Comment> commentList = commentService.findCommentsByEntity(
ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit());
// 评论VO(显示)列表
List<Map<String, Object>> commentVoList = new ArrayList<>();
if (commentList != null) {
for (Comment comment : commentList) {
// 评论VO
Map<String, Object> commentVo = new HashMap<>();
// 评论内容
commentVo.put("comment", comment);
// 评论的作者
commentVo.put("user", userService.findUserById(comment.getUserId()));
// 评论的回复列表
List<Comment> replyList = commentService.findCommentsByEntity(
ENTITY_TYPE_COMMENT, comment.getId(), 0, Integer.MAX_VALUE);
// 回复VO列表
List<Map<String, Object>> replyVoList = new ArrayList<>();
if (replyList != null) {
for (Comment reply : replyList) {
Map<String, Object> replyVo = new HashMap<>();
// 回复的内容
replyVo.put("reply", reply);
// 回复的作者
replyVo.put("user", userService.findUserById(reply.getUserId()));
// 查询到回复的目标
User target = reply.getTargetId() == 0 ?
null : userService.findUserById(reply.getTargetId());
replyVo.put("target", target);
replyVoList.add(replyVo);
}
}
commentVo.put("replys", replyVoList);
// 回复数量
int replyCount = commentService.findCommentCount(ENTITY_TYPE_COMMENT, comment.getId());
commentVo.put("replyCount", replyCount);
commentVoList.add(commentVo);
}
}
model.addAttribute("comments", commentVoList);
return "/site/discuss-detail";
}
CommunityConstant.java
/**
* 实体类型: 帖子
*/
int ENTITY_TYPE_POST = 1;
/**
* 实体类型: 评论
*/
int ENTITY_TYPE_COMMENT = 2;
index.html
3.6、添加评论(涉及事务)
(1)dao层
CommentMapper.java:
增加一个添加评论的方法
int insertComment(Comment comment);
comment-mapper.xml:
<sql id="insertFields">
user_id, entity_type, entity_id, target_id, content, status, create_time
</sql>
<insert id="insertComment" parameterType="Comment">
insert into comment(<include refid="insertFields"></include>)
values(#{userId},#{entityType},#{entityId},#{targetId},#{content},#{status},#{createTime})
</insert>
更新该帖子的评论数量:DiscussPostMapper.java
int updateCommentCount(int id, int commentCount);
discusspost-mapper.java
<update id="updateCommentCount">
update discuss_post set comment_count = #{commentCount} where id = #{id}
</update>
(2)service层
DiscussPostService.java
public int updateCommentCount(int id, int commentCount) {
return discussPostMapper.updateCommentCount(id, commentCount);
}
核心:增加评论的业务(事务管理)CommentService.java
添加评论和更新帖子的评论数量作为一个事务,具有原子性
@Autowired
private SensitiveFilter sensitiveFilter;
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public int addComment(Comment comment) {
if (comment == null) {
throw new IllegalArgumentException("参数不能为空!");
}
// 添加评论
comment.setContent(HtmlUtils.htmlEscape(comment.getContent()));//过滤标签
comment.setContent(sensitiveFilter.filter(comment.getContent()));//敏感词过滤
int rows = commentMapper.insertComment(comment);
// 更新帖子评论数量
if (comment.getEntityType() == ENTITY_TYPE_POST) {
int count = commentMapper.selectCountByEntity(comment.getEntityType(), comment.getEntityId());
discussPostService.updateCommentCount(comment.getEntityId(), count);
}
return rows;
}
(3)controller层
CommentController.java
@Controller
@RequestMapping("/comment")
public class CommentController {
@Autowired
private CommentService commentService;
@Autowired
private HostHolder hostHolder;//获取当前发表评论的用户的信息
//请求路径中要有帖子的id,(添加评论时,页面还是停留在该帖子中)
@RequestMapping(path = "/add/{discussPostId}", method = RequestMethod.POST)
public String addComment(@PathVariable("discussPostId") int discussPostId, Comment comment) {
comment.setUserId(hostHolder.getUser().getId());//设置当前发表评论的用户的id
comment.setStatus(0);//0:有效状态
comment.setCreateTime(new Date());
commentService.addComment(comment);
return "redirect:/discuss/detail/" + discussPostId;//重定向
}
}
discuss-detail.html
3.7、私信列表
(1)dao层
- 创建数据库message:
CREATE TABLE `message` (
`id` int NOT NULL AUTO_INCREMENT,
`from_id` int DEFAULT NULL, #消息的发送者id
`to_id` int DEFAULT NULL, #消息的接受者id
`conversation_id` varchar(45) NOT NULL, #会话id
`content` text,
`status` int 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 AUTO_INCREMENT=408 DEFAULT CHARSET=utf8
conversation_id: 会话id from_id和to_id的组合 111->112和112->111属于同一个会话,这里从小到大来进行表示:111_112。为了以会话id查询、筛选事方便。(冗余数据:因为可以从from_id和to_id得到)
-
entity类:
Message.java
-
mapper:
MessageMapper.java
@Mapper public interface MessageMapper { // 查询当前用户的会话列表,针对每个会话只返回一条最新的私信. List<Message> selectConversations(int userId, int offset, int limit); // 查询当前用户的会话总数量. int selectConversationCount(int userId); // 查询某个会话所包含的私信列表.根据conversationId来查询 List<Message> selectLetters(String conversationId, int offset, int limit); // 查询某个会话所包含的私信数量. int selectLetterCount(String conversationId); // 查询某个会话未读私信的数量,当前用户(userId)的某个会话(conversationId)的未读私信 int selectLetterUnreadCount(int userId, String conversationId); }
-
message-mapper.java
<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="Message"> select <include refid="selectFields"></include> from message where id in (//子查询 select max(id) from message //筛选出最新的消息 where status != 2 //2: 被删除 and from_id != 1 //1:来自系统消息 and (from_id = #{userId} or to_id = #{userId}) group by conversation_id //以conversation_id分组 ) order by id desc limit #{offset}, #{limit} </select> // 查询当前用户的会话总数量. <select id="selectConversationCount" resultType="int"> select count(m.maxid) from ( select max(id) as maxid from message where status != 2 and from_id != 1 and (from_id = #{userId} or to_id = #{userId}) group by conversation_id ) as m </select> // 查询某个会话所包含的私信列表.根据conversationId来查询 <select id="selectLetters" resultType="Message"> select <include refid="selectFields"></include> from message where status != 2 and from_id != 1 and conversation_id = #{conversationId} order by id desc limit #{offset}, #{limit} </select> // 查询某个会话所包含的私信数量. <select id="selectLetterCount" resultType="int"> select count(id) from message where status != 2 and from_id != 1 and conversation_id = #{conversationId} </select> // 查询某个会话未读私信的数量,当前用户(userId)的某个会话(conversationId)的未读私信 <select id="selectLetterUnreadCount" resultType="int"> select count(id) from message where status = 0 // 0:未读 and from_id != 1 and to_id = #{userId} //别人发给当前用户的 <if test="conversationId!=null"> and conversation_id = #{conversationId} </if> </select>
- 测试
(2)service层
MessageService.java
@Service
public class MessageService {
@Autowired
private MessageMapper messageMapper;
// 查询当前用户的会话列表,针对每个会话只返回一条最新的私信.
public List<Message> findConversations(int userId, int offset, int limit) {
return messageMapper.selectConversations(userId, offset, limit);
}
// 查询当前用户的会话总数量.
public int findConversationCount(int userId) {
return messageMapper.selectConversationCount(userId);
}
// 查询某个会话所包含的私信列表.根据conversationId来查询
public List<Message> findLetters(String conversationId, int offset, int limit) {
return messageMapper.selectLetters(conversationId, offset, limit);
}
// 查询某个会话所包含的私信数量.
public int findLetterCount(String conversationId) {
return messageMapper.selectLetterCount(conversationId);
}
// 查询某个会话未读私信的数量,当前用户(userId)的某个会话(conversationId)的未读私信
public int findLetterUnreadCount(int userId, String conversationId) {
return messageMapper.selectLetterUnreadCount(userId, conversationId);
}
}
(3)controller层
MessageController.java
@Controller
public class MessageController {
@Autowired
private MessageService messageService;
@Autowired
private HostHolder hostHolder;//当前用户的信息
@Autowired
private UserService userService;
// ===============================私信列表=======================================
@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()));
//与当前用户产生对话的用户id
int targetId = user.getId() == message.getFromId() ? message.getToId() : message.getFromId();
map.put("target", userService.findUserById(targetId));
conversations.add(map);
}
}
model.addAttribute("conversations", conversations);
// 查询未读消息数量
int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(), null);
model.addAttribute("letterUnreadCount", letterUnreadCount);
return "/site/letter";
}
//======================查看私信详情,路径中包含conversationId========================
@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);
}
}
model.addAttribute("letters", letters);
// 私信目标
model.addAttribute("target", getLetterTarget(conversationId));
// 设置已读状态-----(后面发送私信这节新加的)
List<Integer> ids = getLetterIds(letterList);
if (!ids.isEmpty()) {
messageService.readMessage(ids);
}
return "/site/letter-detail";
}
//获得与当前用户进行私信会话的对象的id
private User getLetterTarget(String conversationId) {
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;
}
3.8、发送私信(异步请求)
(1)dao层
增加发送私信方法:MessageMapper.java:
// 新增消息
int insertMessage(Message message);
// 修改消息的状态
int updateStatus(List<Integer> ids, int status);
message-mapper.xml:
<sql id="insertFields">
from_id, to_id, conversation_id, content, status, create_time
</sql>
<insert id="insertMessage" parameterType="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>
(2)service层
MessageService.java:
//发送新私信
public int addMessage(Message message) {
message.setContent(HtmlUtils.htmlEscape(message.getContent()));//过滤
message.setContent(sensitiveFilter.filter(message.getContent()));
return messageMapper.insertMessage(message);
}
//将私信状态设置为已读
public int readMessage(List<Integer> ids) {
return messageMapper.updateStatus(ids, 1);
}
(3)controller层
MessageController.java
//============================新增一条私信(异步!!!)===========================
@RequestMapping(path = "/letter/send", method = RequestMethod.POST)
@ResponseBody
public String sendLetter(String toName, String content) {
User target = userService.findUserByName(toName);//查询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()) {//id小的放前面
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);
}
在 UserService
中增加通过用户name查询用户信息的方法 findUserByName
public User findUserByName(String username) {
return userMapper.selectByName(username);
}
letter.html
3.9、统一异常处理
spring boot自动提供的异常处理方式:在特定目录位置下templates/error 以错误状态名创建对应的 html
.比如404.html
和 500.html
,当出现对应的错误时会自动跳转至相应的错误页面。
一旦出现了错误,最好还需要记录日志。
controller层
HomeController:
添加error页面的请求
@RequestMapping(path = "/error", method = RequestMethod.GET)
public String getErrorPage() {
return "/error/500";
}
配置类
controller.advice.ExceptionAdvice.java
//扫描范围:只扫描带有Controller注解的bean
@ControllerAdvice(annotations = Controller.class)
public class ExceptionAdvice {
private static final Logger logger = LoggerFactory.getLogger(ExceptionAdvice.class);
@ExceptionHandler({Exception.class})//处理的异常的范围
public void handleException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException {
logger.error("服务器发生异常: " + e.getMessage());
for (StackTraceElement element : e.getStackTrace()) {//element:每条异常信息
logger.error(element.toString());
}
//记录完日志后需要给用户返回页面,需要根据同步请求和异步请求加以区分
//先判断是同步请求还是异步请求,异步请求返回xml\json格式的数据
String xRequestedWith = request.getHeader("x-requested-with");
if ("XMLHttpRequest".equals(xRequestedWith)) {//异步请求
response.setContentType("application/plain;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(CommunityUtil.getJSONString(1, "服务器异常!"));
} else {//重定向
response.sendRedirect(request.getContextPath() + "/error");
}
}
}
优点:不需要在任何一个Controller之上加代码就能解决记录日志的问题
3.10、统一记录日志(AOP)
- Joinpoint: 连接点,表示织入目标对象的哪个具体位置,可以是属性,可以是方法,可以是构造方法、静态块、成员方法里等。
- Target: 目标对象
- Aspect: 切面组件(把公有的业务逻辑封装在里面)
- PointCut: 切点:用表达式来声明,目标对象有这么多位置可以织入代码,那我到底要织入到哪些对象的哪些位置之上(有选择地织入)
- Advice: 通知:实现了具体的业务逻辑,定位更加具体的位置,比如织入到某个方法,需更详细地确认织入方法前,还是方法后,还是抛异常的地方,还是有返回值的地方,
示例
community.aspect.AlphaAspect.java
@Component
@Aspect
public class AlphaAspect {
//切入点:该包下的所有的业务组件的所有的方法所有的参数以及所有的返回值
@Pointcut("execution(* com.nowcoder.community.service.*.*(..))")
public void pointcut() {
}
//定义通知
@Before("pointcut()")
public void before() {
System.out.println("before");
}
@After("pointcut()")
public void after() {
System.out.println("after");
}
//有返回值以后织入
@AfterReturning("pointcut()")
public void afterRetuning() {
System.out.println("afterRetuning");
}
//抛出异常时织入
@AfterThrowing("pointcut()")
public void afterThrowing() {
System.out.println("afterThrowing");
}
//同时在方法的前后都织入
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//joinPoint表示程序织入的部位
//目标方法调用之前的处理逻辑
System.out.println("around before");
//调用目标组件的方法(可能有返回值)
Object obj = joinPoint.proceed();
//目标方法调用之后的处理逻辑
System.out.println("around after");
return obj;
}
}
针对每一个业务组件,相应的方法都会被触发,且业务组件中的代码未做任何修改,降低耦合度。
利用AOP完成项目记录日志的功能
ServiceLogAspect:
在每一个业务组件方法调用之前(前置通知),记录用户[1,2,3,4],在[xxx],访问了[com.newcoder.community.service.xxx()]
@Component
@Aspect
public class ServiceLogAspect {
private static final Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class);
@Pointcut("execution(* com.nowcoder.community.service.*.*(..))")
public void pointcut() {
}
@Before("pointcut()")
public void before(JoinPoint joinPoint) {
// 用户[1.2.3.4],在[xxx],访问了[com.nowcoder.community.service.xxx()].
//利用一个工具类获取Request相关的内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String ip = request.getRemoteHost();
String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
String target = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
logger.info(String.format("用户[%s],在[%s],访问了[%s].", ip, now, target));
}
}