互助交流论坛系统 第三章 Spring Boot实践,开发社区核心功能

8 篇文章 0 订阅
5 篇文章 0 订阅
本文详细介绍了使用前缀树(Trie)算法实现敏感词过滤器的方法,包括原理、代码实现步骤以及在实际项目中的应用,如AJAX异步请求和JSON数据处理。涵盖敏感词的添加、测试、AJAX示例、事务管理和敏感词过滤的前后端处理。
摘要由CSDN通过智能技术生成

过滤敏感词

1. 原理

使用前缀树实现过滤敏感词算法

  • 前缀树
    • 名称:Trie、字典树、查找树
    • 特点:查找效率高,消耗内存大
    • 应用:字符串检索、词频统计、字符串排序等
  • 敏感词过滤器
    • 定义前缀树
    • 根据敏感词,初始化前缀树
    • 编写过滤敏感词的方法

2. 代码实现

创建resources目录下的敏感词列表sensitive-words.txt

创建工具类

在这里插入图片描述

定义前缀树

// 定义前缀树
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 getSuNode(Character c) {
        return subNodes.get(c);
    }
}
``
### 根据敏感词,初始化前缀树
```java 
// 将一个敏感词添加到前缀树中
private void addKeyword(String keyword) {
    TrieNode tempNode = rootNode;
    for (int i = 0; i < keyword.length(); i++) {
        char c = keyword.charAt(i);
        TrieNode suNode = tempNode.getSuNode(c);
        if (suNode == null) {
            // 初始化子节点
            suNode = new TrieNode();
            tempNode.addSubNode(c, suNode);
        }

        // 指向子节点,进入下一轮循环
        tempNode = suNode;

        // 设置结束标识
        if(i == keyword.length() - 1) {
            tempNode.setKeywordEnd(true);
        }
    }
} 

编写测试类

public class SensitiveTests {

    @Autowired
    private SensitiveFilter sensitiveFilter;

    @Test
    public void testSensitiveFilter() {
        String text = "这里可以赌博,可以嫖娼,可以吸毒,可以开票,哈哈哈!";
        text = sensitiveFilter.filter(text);
        System.out.println(text);

        text = "这里可以☆赌☆博☆,可以☆嫖☆娼☆,可以☆吸☆毒☆,可以☆开☆票☆,哈哈哈!";
        text = sensitiveFilter.filter(text);
        System.out.println(text);
    }
}

发布帖子

1. 原理

异步请求:当前网页不刷新,向服务器返回结果,这些结果中提炼的数据对网页进行刷新

  • AJAX(实现异步请求技术)
    • Asynchronous JavaScript and XML
    • 异步的JavaScript与XML,不是一门新技术,只是一个新的术语。
    • 使用AJAX,网页能够将增量更新呈现在页面上,而不需要刷新整个页面。
    • 虽然X代表XML,但目前JSON的使用比XML更加普遍。
    • https://developer.mozilla.org/zh-CN/docs/Web/Guide/AJAX

2. 示例

使用jQuery发送AJAX请求。

引入fastjson依赖

<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>fastjson</artifactId>
   <version>1.2.58</version>
</dependency>

实现工具类:返回json数据

/**
 * 返回json数据
 * 传入编码,字符信息,业务数据
 */
public static String getJSONString(int code, String msg, Map<String, Object> map) {
    JSONObject json = new JSONObject();
    json.put("code", code);
    json.put("msg", msg);
    // 把map对象中的每个键值对传入json对象中
    if(map != null) {
        for(String key : map.keySet()) {
            json.put(key, map.get(key));
        }
    }
    return json.toJSONString();
}
/**
 * 对getJSONString方法进行重载(参数不同)
 */
public static String getJSONString(int code, String msg) {
    return getJSONString(code, msg, null);
}

public static String getJSONString(int code) {
    return getJSONString(code, null, null);
}

编写main方法进行测试

public static void main(String[] args) {
    Map<String, Object> map = new HashMap<>();
    map.put("name", "zhangsan");
    map.put("age", 25);
    System.out.println(getJSONString(0, "ok", map));
}

一个java项目中可以有多个mai方法
java是面向对象的语言,所以java的main函数是写在类里面的,而C/C++等语言是面向过程的语言,main函数是写在类外面的,导致C/C++只能有一个main函数。而java不同,java在运行时会自动寻找main函数,并把找到的第一个main函数作为入口进行执行,而其他的main函数会被当成普通函数来处理。

编写controller方法

@RequestMapping(path = "/ahax", method = RequestMethod.POST)
@ResponseBody
public String textAjax(String name, int age) {
    System.out.println(name);
    System.out.println(age);
    return CommunityUtil.getJSONString(0, "操作成功");
}

在网页编写jquery代码

<script>
    function send() {
        $.post(
            "/community/alpha/ajax",
            {"name":"张三","age":23},
            function(data) {
                // 打印
                console.log(typeof(data));
                console.log(data);

                // 把data转换为json对象
                data = $.parseJSON(data);
                console.log(typeof(data));
                console.log(data.code);
                console.log(data.msg);
            }
        );
    }
</script>

3. 实践

采用AJAX请求,实现发布帖子的功能。

创建mapper接口

@Mapper
public interface DiscussPostMapper {

    List<DiscussPost> selectDiscussPosts(int userId, int offset, int limit);

    // @Param注解用于给参数取别名,
    // 如果只有一个参数,并且在<if>里使用,则必须加别名.
    int selectDiscussPostRows(@Param("userId") int userId);

    int insertDiscussPost(DiscussPost discussPost);

}

实现mapper接口

<!--int insertDiscussPost(DiscussPost discussPost);-->
<insert id="insertDiscussPost" parameterType="DiscussPost">
    insert into discuss_post(<include refid="insertFields"></include>)
    values(#{userId},#{title},#{content},#{type},#{status},#{createTime},#{commentCount},#{score})
</insert>

开发service层

  • addDiscussPost
    • 对参数进行判断
    • 转义HTML标记:替换标签,省得浏览器误认为元素
    • 过滤敏感词
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);

}

开发controller层

@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.setUserId(user.getId());
    post.setTitle(title);
    post.setContent(content);
    post.setCreateTime(new Date());
    discussPostService.addDiscussPost(post);

    // 报错的情况,将来统一处理.
    return CommunityUtil.getJSONString(0, "发布成功!");
}

页面

index.js

function publish() {
   $("#publishModal").modal("hide");

   // 获取标题和内容
   var title = $("#recipient-name").val();
   var content = $("#message-text").val();
   // 发送异步请求(POST)
   $.post(
      CONTEXT_PATH + "/discuss/add",
      {"title":title,"content":content},
      function(data) {
         data = $.parseJSON(data);
         // 在提示框中显示返回消息
         $("#hintBody").text(data.msg);
         // 显示提示框
         $("#hintModal").modal("show");
         // 2秒后,自动隐藏提示框
         setTimeout(function(){
            $("#hintModal").modal("hide");
            // 刷新页面
            if(data.code == 0) {
               window.location.reload();
            }
         }, 2000);
      }
   );

}

帖子详情

1. 具体步骤

  • DiscussPostMapper
  • DiscussPostService
  • DiscussPostController
  • index.html
    • 在帖子标题上增加访问详情页面的链接
  • discuss-detail.html
    • 处理静态资源的访问路径
    • 复用index.html的header区域
    • 显示标题、作者、发布时间、帖子正文等内容

2. 代码实现

创建mapper接口

返回的是DiscussPost

// 查询帖子详情
DiscussPost selectDiscussPostById(int id); 

实现mapper接口

<!--DiscussPost selectDiscussPostById(int id);-->
<select id="selectDiscussPostById" resultType="DiscussPost">
    select <include refid="selectFields"></include> 
    from discuss_post 
    where id = #{id}
</select>

开发service层

public DiscussPost findDiscussPostById(int id) {
    return discussPostMapper.selectDiscussPostById(id);
}

开发controller层

@RequestMapping(path = "/detail/{discussPostId}", method = RequestMethod.GET)
public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model) {
    // 帖子
    DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
    model.addAttribute("post", post);
    // 作者
    User user = userService.findUserById(post.getUserId());
    model.addAttribute("user", user);
    
    return "/site/discuss-detail";
}

处理html

index.html

<a th:href="@{|/discuss/detail/|${map.post.id}"

事务管理

1. 事务

定义

  • 什么是事务
    • 事务是由N步数据库操作序列组成的逻辑执行单元,这系列操作要么全执行,要么全放弃执行。
  • 事务的特性(ACID)
    • 原子性(Atomicity):事务是应用中不可再分的最小执行体。
    • 一致性(Consistency):事务执行的结果,须使数据从一个一致性状态,变为另一个一致性状态。
      一致性状态:事务需要满足数据库相关的约束
    • 隔离性(Isolation):各个事务的执行互不干扰,任何事务的内部操作对其他的事务都是隔离的。
    • 持久性(Durability):事务一旦提交,对数据所做的任何改变都要记录到永久存储器中。

隔离性

程序往往是多线程,因此需要隔离性,判断优先级顺序

  • 常见的并发异常
    • 第一类丢失更新、第二类丢失更新。
    • 脏读、不可重复读、幻读。
  • 常见的隔离级别(既能保证业务的需要,又保持数据库性能)
    • Read Uncommitted:读取未提交的数据。
    • Read Committed:读取已提交的数据。
    • Repeatable Read:不可重复读。
    • Serializable:串行化。(能解决所有的问题,但是需要对数据库进行加锁,下降数据库性能)
      第一类丢失更新:某一个事务的回滚,导致另外一个事务已更新的数据丢失了。
      第二类丢失更新:某一个事务的提交,导致另外一个事务已更新的数据丢失了。
      脏读:某一个事务,读取了另外一个事务未提交的数据。
      不可重复读:某一个事务,对同一个数据前后读取的结果不一致。
      幻读:某一个事务,对同一个表前后查询到的行数不一致。(查询多行数据导致不一致)

隔离级别

在这里插入图片描述

实现机制

  • 悲观锁(数据库)
    悲观锁看待事务非常悲观,如果并发就一定有问题
    • 共享锁(S锁)
      事务A对某数据加了共享锁后,其他事务只能对该数据加共享锁,但不能加排他锁。
    • 排他锁(X锁)
      事务A对某数据加了排他锁后,其他事务对该数据既不能加共享锁,也不能加排他锁。
  • 乐观锁(自定义)
    • 版本号、时间戳等
      在更新数据前,检查版本号是否发生变化。若变化则取消本次更新,否则就更新数据(版本号+1)。

2. Spring事务管理

  • 声明式事务
    • 通过XML配置,声明某方法的事务特征。
    • 通过注解,声明某方法的事务特征。
  • 编程式事务
    • 通过 TransactionTemplate 管理事务,并通过它执行数据库的操作。
      一般选择第一种方法,如果业务复杂,又只想管理中间某一部分,就使用第二种方法

3. 代码模拟

声明式事务

@Transactional:选择一个默认的事务管理的方式

  • propagation: 事物的传播机制(业务方法a可能会调用业务方法b,这两个方法可能都会加上transcational注解,为了解决以谁为准的问题
    • REQUIRED:支持当前事务(外部事务,a调用b,a就是b的外部事务),如果不存在,则创建新事务
    • REQUIRES_NEW:创建一个新事务,并且暂停当前事务(外部事务)
    • NESTED:如果当前存在事务(外部事务),则嵌套在该事务中执行(独立的提交和回滚),如果外部事务不存在,则和REQUIRED一样
      save1:
  1. 新增用户
  2. 新增帖子
  3. 模拟报错回滚
@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());
    disscussPostMapper.insertDiscussPost(post);

    // 模拟报错:把"abc"字符串转为整数
    Integer.valueOf("abc");

    return "ok";
}

编程式事务

有些库函数(library function)却要求应用先传给它一个函数,好在合适的时候调用,以完成目标任务。这个被传入的、后又被调用的函数就称为回调函数(callback function)。

@Autowired
private TransactionTemplate transactionTemplate;

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 transactionStatus) {
            // 新增用户
            User user = new User();
            user.setUsername("beta");
            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("你好");
            post.setContent("我是新人");
            post.setCreateTime(new Date());
            disscussPostMapper.insertDiscussPost(post);

            // 模拟报错:把"abc"字符串转为整数
            Integer.valueOf("abc");
            
            return "ok";
        }
    });

显示评论

1. 步骤

  • 数据层
    entity_type: 评论可以评论原贴,也可以评论评论,entity_type只评论对象的类型
    entity_id: 评论的对象
    target_id : 评论指向的人
    status : 状态,删没删除?
    在这里插入图片描述

    • 根据实体查询一页评论数据。
    • 根据实体查询评论的数量。
  • 业务层

    • 处理查询评论的业务。
    • 处理查询评论数量的业务。
  • 表现层

    • 显示帖子详情数据时,同时显示该帖子所有的评论数据。

2. 代码实现

数据层

新建实体类Comment
创建mapper接口
public interface CommentMapper {

    /**
     *  根据实体查询(是帖子的评论?评论的评论?)
     *   offset&limint: 为了分页
      */
    List<Comment> selectCommentByEntity(int entityType, int entityId, int offset, int limit);

    /**
     * 查询数据的条目数
     */
    int selectCountByEntity(int entityType, int entityId);
}
实现maper接口
  • limit N,M : 相当于 limit M offset N , 从第 N 条记录开始, 返回 M 条记录
<sql id="selectFields">
       id, user_id, entity_type, entity_id, target_id, content, status, create_time
   </sql>

    <!--List<Comment> selectCommentByEntity(int entityType, int entityId, int offset, int limit);-->
    <select id="selectCommentByEntity" 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>

业务层

public class CommentService {

    @Autowired
    private CommentMapper commentMapper;

    /**
     * 查询某页数据的集合
     */
    public List<Comment> findCommentByEntity(int entityType, int entityId, int offset, int limit) {
        return commentMapper.selectCommentByEntity(entityType, entityId, offset, limit);
    }

    /**
     * 查询评论的数量
     */
    public int findCommentCount(int entityTpye, int entityId) {
        return commentMapper.selectCountByEntity(entityTpye, entityId);
    }

}

表现层

controller层

在DiscussPostController上面写,进行补充即可

 List<Comment> commentList = commentService.findCommentByEntity(
            ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit());
    // 评论VO列表:view object
    List<Map<String, Object>> commentVoList = new ArrayList<>();
    if(commentList != null) {
        for (Comment comment : commentList) {
            // 一个评论的vo
            Map<String, Object> commentVo = new HashMap<>();
            // 往vo里添加评论
            commentVo.put("comment", comment);
            // 往vo里加作者
            commentVo.put("user", userService.findUserById(comment.getUserId()));

            // 回复列表
            List<Comment> replyList = commentService.findCommentByEntity(
                    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";
}
templates

index.html

<span th:text="map.post.commentCount">7</span>

添加评论

1. 步骤

  • 数据层
    • 增加评论数据。
    • 修改帖子的评论数量。
  • 业务层
    • 处理添加评论的业务:先增加评论、再更新帖子的评论数量。
  • 表现层
    • 处理添加评论数据的请求。
    • 设置添加评论的表单。

2. 代码实现

数据层

增加评论数据
/**
 * 增加评论
 */
int insertComment(Comment comment);
<!--int insertComment(Comment comment);-->
<!--新增则需要声明参数类型-->
<select id="insertComment" parameterType="Comment">
    insert into comment(<include refid="insertFields"></include>>)
    values(#{userId},#{entityType},#{entityId},#{targetId},#{content},#{status},#{createTime})
</select>
修改帖子的评论数量
/**
 * 增加更新条数
 */
int updateCommentCount(int id, int commentCount);
<update id="updateCommentCount">
    update discuss_post set comment_count = #{commentCount} where id = #{id}
</update>

业务层

public int updateCommentCount(int id, int commentCount) {
    return discussPostMapper.updateCommentCount(id, commentCount);
}

处理添加评论的业务:

  • 先增加评论
  • 再更新帖子的评论数量。
    两次事务管理:增加评论、添加评论的数量,希望要么全成功,要么全失败
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public int addComment(Comment comment) {
    if(comment  == null) {
        throw new IllegalArgumentException("参数不能为空!");
    }
    // 对内容进行过滤:html标签的过滤 + 敏感词过滤
    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;
}

表现层

controller层
  • 处理添加评论数据的请求。
  • 设置添加评论的表单。
// 回帖之后是重定向到原来的帖子那里,通过路径传过来
@RequestMapping(path = "/add/{discussPostId}", method = RequestMethod.POST)
public String addComment(@PathVariable("discussPostId") int discussPostId, Comment comment) {
    comment.setUserId(hostHolder.getUser().getId());
    comment.setStatus(0); // 表示有效
    comment.setCreateTime(new Date());
    commentService.addComment(comment);

    // 重定向到帖子详情页
    return "redirect:/discuss/detail/" + discussPostId;
}
html页面
  • 给帖子进行回帖
  • 给评论进行评论
  • 给楼中楼进行回复

私信列表

1. 步骤

from_id :消息的发送方 1-系统通知(不是私信)
to_id :消息的接收方
status : 0-未读 1-已读 2-删除
conversation_id : 会话id 111_112(111和112的会话,小在前,大在后,不区分方向)
在这里插入图片描述

  • 私信列表
    • 查询当前用户的会话列表(我和某个人的多次对话,称为一次会话),每个会话只显示一条最新的私信。
    • 支持分页显示。
  • 私信详情
    • 查询某个会话所包含的私信。
    • 支持分页显示。

2. 代码实现

数据层

构造实体类Message
创建mapper接口``
@Mapper
public interface MessageMapper {

    // 查询当前用户的会话列表,针对每个会话只返回一条最新的私信
    List<Message> selectConversations(int userId, int offset, int limit);

    // 查询当前用户的会话数量
    int selectConversationCount(int userId);

    // 查询某个会话所包含的私信列表
    List<Message> selectLetters(String conversationId, int offset, int limit);

    // 查询某个会话所包含的私信数量
    int selectLetterCount(String conversationId);

    // 查询未读的私信数量
    // 把String conversationId作为动态的参数去用,传了就用,不传就不用,这样可以实现两种方式的业务
    int selectLetterUnreadCount(int userId, String conversationId);
实现mapper接口(有点复杂)
<sql id="selectFields">
    id, from_id, to_id, conversation_id, content, status, create_time
</sql>

<!--List<Message> selectConversations(int userId, int offset, int limit);-->
<select id="selectConversations" resultType="Message">
    select <include refid="selectFields"></include>
    from message
    where id in (
        -- 得到每条会话最新的id,所以用max
        -- 查询当前用户的会话列表,针对每个会话只返回一条最新的私信
        SELECT MAX(id) FROM message
        WHERE status != 2
        AND from_id != 1
        AND (from_id = #{userId} or to_id = #{userId})
        GROUP BY conversation_id
    )
    order by id desc
    limit #{offset}, #{limit}
</select>

<!--int selectConversationCount(int userId);-->
<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>

<!--List<Message> selectLetters(String conversationId, int offset, int limit);-->
<!--查询某个会话所包含的私信列表-->
<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>

<!--int selectLetterCount(String conversationId);-->
<!--查询某个会话所包含的私信数量-->
<select id="selectLetterCount" resultType="int">
    select count(id)
    from message
    where status != 2
    and from_id != 1
    and conversation_id = #{conversationId}
</select>

<!--int selectLetterUnreadCount(int userId, String conversationId);-->
<!--未读一定是当前用户发消息给我的,一定是to_id-->
<select id="selectLetterUnreadCount" resultType="int">
    select count(id)
    from message
    where status = 0
    and from_id != 1
    and to_id = #{userId}
    <if test="conversationId!=null">
        and conversation_id = #{conversationId}
    </if>
</select>
创建测试类
@Test
public void testSelectLetters() {
    List<Message> list = messageMapper.selectConversations(111, 0, 20);
    for(Message message : list) {
        System.out.println(message);
    }

    int count = messageMapper.selectConversationCount(111);
    System.out.println(count);

    List<Message> list1 = messageMapper.selectLetters("111_112", 0, 10);
    for(Message message : list1) {
        System.out.println(message);
    }

    count = messageMapper.selectLetterCount("111_112");
    System.out.println(count);

    count = messageMapper.selectLetterUnreadCount(131, "111_131");
    System.out.println(count);

}

业务层

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

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

    public int findLetterUnreadCount(int userId, String conversationId) {
        return messageMapper.selectLetterUnreadCount(userId, conversationId);
    }
}

表现层

controller层
  • 设置分页信息
  • 添加会话列表
  • 查询未读消息数量
@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";
}
  • 私信详情
    • 查询某个会话所包含的私信。
    • 支持分页显示。
@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)));

    return "/site/letter-detail";
}

/**
 * conversationId由两段构成,哪段和用户id不一样哪一段就是目标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(id1);
    }
}

发送私信

1. 步骤

  • 发送私信
    • 采用异步的方式发送私信,发送私信时要自动填写发私信人的名字
    • 系统判断成功还是失败,发送通知
    • 发送成功后刷新私信列表。
  • 设置已读
    • 访问私信详情时,将显示的私信设置为已读状态。

2. 代码实现

数据层

创建mapper接口
// 增加一个消息
int insertMessage(Message message);

// 把未读消息设为已读,把status从0改到1。设置删除
int updateStatus(List<Integer> ids, int status);
实现mapper接口
<!--// 增加一个消息-->
<!--int insertMessage(Message message);-->
<insert id="insertMessage" parameterType="Message" keyProperty="id">
    insert into message(<include refid="insertFields"></include>)
    values (#{fromId},#{toId},#{conversationId},#{content},#{status},#{createTime})
</insert>

<!--// 把未读消息设为已读,把status从0改到1。设置删除-->
<!--int updateStatus(List<Integer> ids, int status);-->
<update id="updateStatus">
    update message set status = #{status}
    where id in
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</update>

业务层

  • 增加message的方法
  • 改变私信状态的方法
<!--// 增加一个消息-->
<!--int insertMessage(Message message);-->
<insert id="insertMessage" parameterType="Message" keyProperty="id">
    insert into message(<include refid="insertFields"></include>)
    values (#{fromId},#{toId},#{conversationId},#{content},#{status},#{createTime})
</insert>

<!--// 把未读消息设为已读,把status从0改到1。设置删除-->
<!--int updateStatus(List<Integer> ids, int status);-->
<update id="updateStatus">
    update message set status = #{status}
    where id in
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</update>

表现层

@RequestMapping(path = "/letter/send", method = RequestMethod.POST)
@ResponseBody
public 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());
    // 因为ConversationId统一是小的在前大的在后
    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);
}
  • 设置已读
    • 访问私信详情时,将显示的私信设置为已读状态。
/**
 * 得到集合中未读消息的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()) {
                ids.add(message.getId());
            }
        }
    }
    return ids;
}

统一异常处理

1. 原理

数据层出现异常抛到业务层,业务层出现异常抛到表现层,因此统一异常处理是针对表现层而言的

  • @ControllerAdvice
    • 用于修饰类,表示该类是Controller的全局配置类。
    • 在此类中,可以对Controller进行如下三种全局配置:异常处理方案、绑定数据方案、绑定参数方案。
  • @ExceptionHandler
    • 用于修饰方法,该方法会在Controller出现异常后被调用,用于处理捕获到的异常。
  • @ModelAttribute
    • 用于修饰方法,该方法会在Controller方法执行前被调用,用于为Model对象绑定参数。
  • @DataBinder
    • 用于修饰方法,该方法会在Controller方法执行前被调用,用于绑定参数的转换器。
      springMVC自内置很多底层转换器

2. 实现

  • 错误页面摆放位置
    在这里插入图片描述
  • 配置错误页面
  • 新建advice.ExceptionAdvice
  • 使用@ControllerAdvice注解
// ControllerAdvice 只去扫描带有controller的bean
@ControllerAdvice(annotations = Controller.class)
public class ExceptionAdvice {

    private static final Logger logger = LoggerFactory.getLogger(ExceptionAdvice.class);

    // Exception是所有异常的父类,则表示所有的方法都用ExceptionHandler处理
    @ExceptionHandler({Exception.class})
    public void handleException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException {
        logger.error("服务器发生异常:" + e.getMessage());
        // 每个element记录一条异常的信息
        for(StackTraceElement element: e.getStackTrace()) {
            logger.error(element.toString());
        }
        String xRequestWith = request.getHeader("x-requested-with");
        // 如果等于这个值,就是异步请求,普通请求期待返回html
        if("XMLHttpRequest".equals(xRequestWith)) {
            // response.setContentType(MIME)的作用是使客户端浏览器,区分不同种类的数据,
            // 并根据不同的MIME调用浏览器内不同的程序嵌入模块来处理相应的数据。
            response.setContentType("application/plain;charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.write(CommunityUtil.getJSONString(1, "服务器异常!"));
        } else {
            response.sendRedirect(request.getContextPath() + "/error");
        }
    }
} 

统一记录日志

1. 需求

纪录日志需要记录异常情况和不异常情况,所以不能用统一异常处理
拦截器是针对控制层而言的,记录日志是可能针对业务层组件或者数据访问层而言的,所以也不能用拦截器

  • 帖子模块
  • 评论模块
  • 消息模块

2. AOP

概念

  • Aspect Oriented Programing,即面向方面(切面)编程。
  • AOP是一种编程思想,是对OOP(面向对象)的补充,可以进一步提高编程的效率。

oop思想:封装bean去调用
在这里插入图片描述

术语

  • target:已程序中已经开发好的bean
  • JoinPoint(连接点):能够被织入代码的位置(属性、构造器、静态块、成员方法)
  • aspect(切面): 另外一个额外的bean。封装业务需求的组件就是aspect
    整个编程过程就是面向aspect编程
    • Pointcut:声明切点,声明到底织入哪些对象的哪些位置
    • advice:实现具体的系统逻辑,具体要做什么?位置在哪里?
  • 利用框架提供置入功能(weaving),把aspect织入到不同的组件中
    • 编译时:织入方式越原始,速度越快,但是可能有些特殊情况下处理得不精细
    • 运行时:效率低

在这里插入图片描述

实现

  • AspectJ
    • AspectJ是语言级的实现,它扩展了Java语言,定义了AOP语法。
    • AspectJ在编译期织入代码,它有一个专门的编译器,用来生成遵守Java字节码规范的class文件。
      支持所有的连接点
  • Spring AOP
    • Spring AOP使用纯Java实现,它不需要专门的编译过程,也不需要特殊的类装载器。
    • Spring AOP在运行时通过代理的方式织入代码,只支持方法类型的连接点(只能将方面组件的代码织入到方法中)。
    • Spring支持对AspectJ的集成。

Spring AOP

  • JDK动态代理
    代理:对某一个对象生成代理对象,调用的时候织入代理对象而不是原始对象
    • Java提供的动态代理技术,可以在运行时创建接口的代理实例。
    • Spring AOP默认采用此种方式,在接口的代理实例中织入代码。
  • CGLib动态代理
    • 采用底层的字节码技术,在运行时创建子类代理实例。
    • 当目标对象不存在接口时,Spring AOP会采用此种方式,在子类实例中织入代码。

3. 代码实现

demo

@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 afterReturning() {
        System.out.println("afterReturning");
    }

    /**
     * 在抛异常后织入
     */
    @AfterThrowing("pointcut()")
    public void afterThrowing() {
        System.out.println("afterThrowing");
    }

    /**
     * 在前后都织入逻辑
     * 参数ProceedingJoinPoint就是连接点-目标织入的部位
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("around before");
        // 利用oinPoint.proceed()调用目标组件的方法
        Object obj = joinPoint.proceed();
        System.out.println("around after");
        return obj;
    }
}

在这里插入图片描述

实现记录日志功能

在业务组件一开始记录日志
用户xx 在xx时刻 访问xx功能

@Component
@Aspect
public class ServiceLogAspect {

    private static final Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class);

    @Pointcut("execution(* com.nowcoder.community.service.*.*(..))")
    public void pointcut() {

    }

    /**
     * 在方法前记录日志
     * @param joinPoint 连接点指的是方法织入的目标
     */
    @Before("pointcut()")
    public void before(JoinPoint joinPoint) {
        // 用户[1.2.3.4],在[xxx],访问了[com.newcoder.community.service.xxx()].
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // ip
        String ip = request.getRemoteHost();
        // 日期
        String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        // 方法名
        // joinPoint.getSignature().getDeclaringTypeName() 获得的是类名,还要在后面拼上方法名
        String target = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();

        logger.info(String.format("用户[%s],在[%s],访问了[%s].", ip, now, target));

    }
}

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值