Postalk

0. 简介

这个项目是一个基于Spring boot, Mybatis, Druid, MySQL技术的web应用,其功能为发帖+聊天。该文章将会记录开发过程以及在过程之中所遇到的问题。

1. 设计部分

数据库设计

为了使开发更为顺利,我首先按照各实体类之间的关系,设计了该ER图。

Postalk_ER

User表:

id username password sex age phone_number email registration_date
序号 用户名 密码 性别(F表示女性,M表示男性) 年龄 电话号码 电子邮箱 注册时间

Text表:

id uid content time
序号 用户序号 文本内容 发文时间

Comment表:

tid pid
对应文本的序号 对应评价文本的序号

Post表:

tid
对应文本的序号

Like表:

uid tid
对应用户的序号 对应文本的序号

Friend表:

uid_1 uid_2 since_time
用户1的序号 用户2的序号 从何时开始成为朋友

Conversation表:

id to_user from_user conversation_id time content read
该段对话的序号 发送人序号 接收人序号 相同两人之间的对话集合的序号 对话时间 对话内容 是否已读

RESTful风格架构设计

功能 请求URI 请求方式
用户登录 /user/login GET
跳转到添加用户界面 /user/ GET
查询用户 /user/{id} GET
用户注册 /user/ POST
跳转到对应用户的主页 /user/myPage GET
跳转到添加帖子界面 /post/ GET
添加帖子 /post/ POST
查询所有帖子 /posts/ GET
查询所有该id对应的用户发的帖子 /post/{id} GET
修改帖子 /post/ PUT
删除帖子 /post/{id} DELETE
点赞或取消赞 /like/ POST
获得当前登录用户对应的所有朋友名单 /friends/ GET
跳转到朋友搜索界面 /friend/ GET
根据输入条件查找用户 /friend/search/ GET
添加朋友 /friend/ POST
删除朋友 /friend/{id} DELETE

2. 搭建Mybatis+SpringBoot基础框架

配置Druid数据源

  1. 首先引入Druid的Maven依赖

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.22</version>
    </dependency>
    
  2. 然后在application.yml中配置Datasource

    spring:
      datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/postalk?useUnicode=true&characterEncoding=utf-8&relaxAutoCommit=true&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=GMT%2B8
        password: root
        username: root
        druid:
          initial-size: 5
          min-idle: 5
          maxActive: 20
          maxWait: 60000
          timeBetweenEvictionRunsMillis: 60000
          minEvictableIdleTimeMillis: 300000
          validationQuery: SELECT 1
          testWhileIdle: true
          testOnBorrow: false
          testOnReturn: false
          poolPreparedStatements: true
          maxPoolPreparedStatementPerConnectionSize: 20
          filters: stat,wall,slf4j
          connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
          web-stat-filter:
            enabled: true
            url-pattern: "/*"
            exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
          stat-view-servlet:
            url-pattern: "/druid/*"
            reset-enable: false
            login-username: admin
            login-password: 123456
            enabled: true
    

实现各实体类

package xyz.aiinirii.postalk.bean;

import lombok.Data;
import lombok.EqualsAndHashCode;

import java.util.Date;

@Data
public class User {

    private int id;
    private String username;
    private String password;
    private String sex;
    private int age;
    private String phoneNumber;
    private String email;
    private Date registrationDate;
}

@Data
public abstract class Text {

    private int id;
    private String content;
    private Date time;

    private User user;
}

@EqualsAndHashCode(callSuper = true)
@Data
public class Comment extends Text{

    private Text text;
}

@EqualsAndHashCode(callSuper = true)
@Data
public class Post extends Text{
}

@Data
public class Like {

    private User user;
    private Text text;
}

@Data
public class Friend {

    private User user1;
    private User user2;
    private Data sinceTime;
}

@Data
public class Conversation {

    private int id;
    private User toUser;
    private User fromUser;
    private int conversationId;
    private Date time;
    private String content;
    private boolean read;
}

3. 用户模块

实现User的CURD操作

Mapper 实现

package xyz.aiinirii.postalk.mapper;

import org.apache.ibatis.annotations.*;
import org.springframework.stereotype.Repository;
import xyz.aiinirii.postalk.bean.User;

import java.util.List;

/**
 * @author AIINIRII
 */
@Mapper
@Repository
public interface UserMapper {

    @Select("select * from user")
    List<User> findAllUser();

    @Select("select * from user where id=#{id}")
    User findUserById(Integer id);

    @Select("delete from user where id=#{id}")
    int deleteUserById(Integer id);

    @Options(useGeneratedKeys = true, keyProperty = "id")
    @Insert("insert into user(username, password, sex, age, phone_number, email, registration_date) values (#{username}, #{password}, #{sex}, #{age}, #{phoneNumber}, #{email}, #{registrationDate})")
    void insertUser(User user);

    @Insert("update user set username=#{username}, password=#{password}, sex=#{sex}, age=#{age}, phone_number=#{phoneNumber}, email=#{email}, registration_date=#{registration_date} where id=#{id}")
    int updateUser(User user);
}

登录注册功能实现

Controller类代码实现

package xyz.aiinirii.postalk.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import xyz.aiinirii.postalk.bean.User;
import xyz.aiinirii.postalk.service.UserService;

import java.util.LinkedList;
import java.util.List;

/**
 * @author AIINIRII
 */
@Controller
public class UserController {

    UserService userService = new UserService();

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    @PostMapping("/user/login")
    public ModelAndView checkUser(User user, ModelAndView modelAndView){
        int res = userService.checkUser(user);
        modelAndView.addObject("checkResult", res);
        if (res == 0) {
            modelAndView.setViewName("/post/index");
        } else {
            modelAndView.setViewName("/index");
        }
        return modelAndView;
    }

    @GetMapping("/user/{id}")
    public ModelAndView findUserById(@PathVariable("id") Integer id, ModelAndView modelAndView) {
        User user = userService.findUserById(id);
        modelAndView.setViewName("user/list");
        List<User> users = new LinkedList<>();
        users.add(user);
        modelAndView.addObject("users", users);
        return modelAndView;
    }

    @GetMapping("/user")
    public String toRegisterPage(){
        return "user/update";
    }

    @PostMapping("/user")
    public ModelAndView registerUser(User user, ModelAndView modelAndView) {

        int res = userService.registerUser(user);
        if (res == 0) {
            // success insert, go to login page
            modelAndView.setViewName("/index");
        } else if (res == 1){
            // the username is used, go back to the register page
            modelAndView.addObject("userR", user);
            modelAndView.setViewName("/user/update");
        }
        return modelAndView;
    }
}

前端代码实现(首页)

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">
    <title>Signin Template for Bootstrap</title>
    <!-- Bootstrap core CSS -->
    <link href="/asserts/css/bootstrap.min.css" rel="stylesheet">
    <!-- Custom styles for this template -->
    <link href="/asserts/css/signin.css" rel="stylesheet">
</head>

<body class="text-center">
<div>
<form class="form-signin" method="post" th:action="@{/user/login}">
    <img class="mb-4" src="/asserts/img/bootstrap-solid.svg" alt="" width="72" height="72">
    <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
    <p style="color: #b21f2d" th:if="${checkResult == 1}">wrong password</p>
    <p style="color: #b21f2d" th:if="${checkResult == 2}">no such user</p>
    <label class="sr-only">Username</label>
    <label>
        <input name="username" type="text" class="form-control" placeholder="Username" required="" autofocus=""/>
    </label>
    <label class="sr-only">Password</label>
    <label>
        <input name="password" type="password" class="form-control" placeholder="Password" required=""/>
    </label>
    <div class="checkbox mb-3">
        <label>
            <input type="checkbox" value="remember-me"/> Remember me
        </label>
    </div>
    <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
</form>
<form class="form-signin" action="/user/" method="get">
    <button class="btn btn-lg btn-success btn-block" type="submit">Register</button>
</form>
<p class="mt-5 mb-3 text-muted">© 2019-2020</p>
</div>
</body>

</html>

前端代码实现(注册界面)

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<style>
    #wrapper {
        width: 60%;
        margin: 0 auto;
        border: 0;
    }
</style>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Register</title>
    <!-- Bootstrap core CSS -->
    <link href="/asserts/css/bootstrap.min.css" rel="stylesheet">
    <!-- Custom styles for this template -->
    <!--<link href="/asserts/css/signin.css" rel="stylesheet">-->
</head>
<body>
<div id="wrapper">
    <form class="needs-validation" novalidate method="post" action="/user">
        <div class="form-group row">
            <label for="inputUsername" class="col-sm-2 col-form-label">User name*</label>
            <div class="col-sm-10 mb-3">
                <input name="username" type="text" class="form-control" id="inputUsername" th:value="${userR!=null}?${userR.username}" required>
                <div class="valid-feedback">
                    Looks good!
                </div>
                <div class="invalid-feedback">
                    Please enter a valid username.
                </div>
            </div>
        </div>
        <div class="form-group row">
            <label for="inputPassword" class="col-sm-2 col-form-label">Password*</label>
            <div class="col-sm-10">
                <input name="password" type="password" class="form-control" id="inputPassword" required>
                <div class="valid-feedback">
                    Looks good!
                </div>
                <div class="invalid-feedback">
                    Please enter a valid password.
                </div>
            </div>
        </div>
        <fieldset class="form-group">
            <div class="row">
                <legend class="col-form-label col-sm-2 pt-0">Gender*</legend>
                <div class="col-sm-10">
                    <div class="form-check">
                        <input class="form-check-input" type="radio" name="sex" id="gridRadios1" value="M"  th:checked="${userR!=null}?${userR.sex=='M'}" checked>
                        <label class="form-check-label" for="gridRadios1">
                            Male
                        </label>
                    </div>
                    <div class="form-check">
                        <input class="form-check-input" type="radio" name="sex" id="gridRadios2" th:checked="${userR!=null}?${userR.sex=='F'}" value="F">
                        <label class="form-check-label" for="gridRadios2">
                            Female
                        </label>
                    </div>
                </div>
            </div>
        </fieldset>
        <div class="form-group row">
            <label for="inputAge" class="col-sm-2 col-form-label">Age*</label>
            <div class="col-sm-10">
                <input name="age" type="number" class="form-control" id="inputAge" th:value="${userR!=null}?${userR.age}" required>
                <div class="valid-feedback">
                    Looks good!
                </div>
                <div class="invalid-feedback">
                    Please enter a valid age.
                </div>
            </div>
        </div>
        <div class="form-group row">
            <label for="inputPhoneNumber" class="col-sm-2 col-form-label">Phone number</label>
            <div class="col-sm-10">
                <input name="phoneNumber" type="text" class="form-control" th:value="${userR!=null}?${userR.phoneNumber}" id="inputPhoneNumber">
                <div class="valid-feedback">
                    Looks good!
                </div>
                <div class="invalid-feedback">
                    Please enter a valid phone number.
                </div>
            </div>
        </div>
        <div class="form-group row">
            <label for="inputEmail" class="col-sm-2 col-form-label">Email</label>
            <div class="col-sm-10">
                <input name="email" type="email" class="form-control" th:value="${userR!=null}?${userR.email}" id="inputEmail">
                <div class="valid-feedback">
                    Looks good!
                </div>
                <div class="invalid-feedback">
                    Please enter a valid email.
                </div>
            </div>
        </div>
        <div class="form-group row">
            <div class="col-sm-10">
                <button type="submit" class="btn btn-primary">Sign in</button>
            </div>
        </div>
    </form>
</div>
</body>
<script>
    // Example starter JavaScript for disabling form submissions if there are invalid fields
    (function () {
        'use strict';
        window.addEventListener('load', function () {
            // Fetch all the forms we want to apply custom Bootstrap validation styles to
            var forms = document.getElementsByClassName('needs-validation');
            // Loop over them and prevent submission
            var validation = Array.prototype.filter.call(forms, function (form) {
                form.addEventListener('submit', function (event) {
                    if (form.checkValidity() === false) {
                        event.preventDefault();
                        event.stopPropagation();
                    }
                    form.classList.add('was-validated');
                }, false);
            });
        }, false);
    })();
</script>
</html>

4. 帖子模块

基本的增删改查

先贴上效果图:

  • 主页面:可以看到平台上所有用户的帖子 - image-20200620132434280

  • 用户主页面:可以看到自己发的所有帖子,而且能够进行修改和删除 - image-20200620133505658

  • 修改界面:这里用户可以编辑内容并提交 - image-20200620133609830

  • 发帖页面:这里可以发帖 - image-20200620133748133

因为代码太多,这里就不贴出来了,主要说几个遇到的问题:

  1. 基于注解的懒加载

        @Select("select * from post left join text t on post.tid = t.id where tid = #{id}")
        @Results(id = "findPostById",
                value = {
                        @Result(id = true, column = "tid", property = "id"),
                        @Result(column = "content", property = "content"),
                        @Result(column = "time", property = "time"),
                        @Result(column = "uid", property = "user", 
                                one = @One(select = "xyz.aiinirii.postalk.mapper.UserMapper.findUserById", 
                                           fetchType = FetchType.LAZY))
                }
        )
        Post findPostById(Integer id);
    

    其中@Result(column = "uid", property = "user", one = @One(select = "xyz.aiinirii.postalk.mapper.UserMapper.findUserById", fetchType = FetchType.LAZY))所实现的效果是根据findUserById这个类,根据提供的uid查询到user对象,并且只有当user被使用了,即调用了getUser()方法后才会执行查询。

  2. 如何使用HTML发送put、delete请求

    其中前端代码为:

    <form action="/post/" method="post" class="needs-validation" novalidate>
        <input type="hidden" name="_method" value="put" th:if="${post!=null}"/>
        <input type="hidden" name="id" th:value="${post.getId()}" th:if="${post!=null}"/>
    

    这里给的是一个发送put请求的例子,其实form表单是不支持发送put请求的,但是我们可以加一个<input type="hidden" name="_method" value="put" th:if="${post!=null}"/>在form表单中,其中字段名必须为_methodvalue应该为希望的请求方式。而这种发送请求的方式,则需要服务端进行form表单的解析,而spring则可以做到这一点,但是需要配置,在application.yml文件中:

    spring:
      mvc:
        hiddenmethod:
          filter:
            enabled: true
    

点赞功能实现

  • 赞数显示

    思路:每次加载页面时自动查询赞数,并显示在页面上。

    相关LikeMapper代码:

    @Mapper
    @Repository
    public interface LikeMapper {
    
        @Select("select * from `like` where tid = #{tid}")
        @Results(
                id = "findLikeByTId",
                value = {
                        @Result(id = true, column = "tid", property = "text", one = @One(select = "xyz.aiinirii.postalk.mapper.TextMapper.findTextById", fetchType = FetchType.LAZY)),
                        @Result(id = true, column = "uid", property = "user", one = @One(select = "xyz.aiinirii.postalk.mapper.UserMapper.findUserById", fetchType = FetchType.LAZY))
                }
        )
        List<Like> findLikeByTId(Integer tid);
    }
    

    除此之外我们还需将PostPostMapper进行调整,每次查询Post时使用懒加载的方式(因为其实需要用到Like的情况并不多)去加载Post中的likes

    @Data
    public abstract class Text implements Serializable {
    
        private Integer id;
        private String content;
        private Date time;
        private boolean anonymous = false;
    
        private User user;
        // 新加入likes属性
        private List<Like> likes;
    }
    

    下面是PostMapper类,主要改动是在每个查询方法上的@Result内加入了@Result(id = true, column = "tid", property = "likes", many = @Many(select = "xyz.aiinirii.postalk.mapper.LikeMapper.findLikeByTId", fetchType = FetchType.LAZY))

    @Mapper
    @Repository
    public interface PostMapper {
      @Select("select * from post p left join text t on p.tid = t.id where uid = #{id} ORDER BY t.time DESC")
        @Results(id = "findAllPostByUId",
                value = {
                        @Result(id = true, column = "tid", property = "id"),
                        @Result(column = "content", property = "content"),
                        @Result(column = "time", property = "time"),
                        @Result(column = "uid", property = "user", one = @One(select = "xyz.aiinirii.postalk.mapper.UserMapper.findUserById", fetchType = FetchType.LAZY)),
                        @Result(id = true, column = "tid", property = "likes", many = @Many(select = "xyz.aiinirii.postalk.mapper.LikeMapper.findLikeByTId", fetchType = FetchType.LAZY))
                }
        )
        List<Post> findAllPostByUId(Integer id);
    
        @Select("select * from post left join text t on post.tid = t.id where tid = #{id}")
        @Results(id = "findPostById",
                value = {
                        @Result(id = true, column = "tid", property = "id"),
                        @Result(column = "content", property = "content"),
                        @Result(column = "time", property = "time"),
                        @Result(column = "uid", property = "user", one = @One(select = "xyz.aiinirii.postalk.mapper.UserMapper.findUserById", fetchType = FetchType.LAZY)),
                        @Result(id = true, column = "tid", property = "likes", many = @Many(select = "xyz.aiinirii.postalk.mapper.LikeMapper.findLikeByTId", fetchType = FetchType.LAZY))
                }
        )
        Post findPostById(Integer id);
    
        @Select("select * from post p left join text t on p.tid = t.id ORDER BY t.time DESC")
        @Results(id = "findAllPost",
                value = {
                        @Result(id = true, column = "tid", property = "id"),
                        @Result(column = "content", property = "content"),
                        @Result(column = "time", property = "time"),
                        @Result(column = "uid", property = "user", one = @One(select = "xyz.aiinirii.postalk.mapper.UserMapper.findUserById", fetchType = FetchType.LAZY)),
                        @Result(id = true, column = "tid", property = "likes", many = @Many(select = "xyz.aiinirii.postalk.mapper.LikeMapper.findLikeByTId", fetchType = FetchType.LAZY))
                }
        )
        List<Post> findAllPost();
    }
    

    这样一来,每次我们需要查询Post的赞数时,我们只需要调用post.getLikes().size()方法。

  • 点击按钮增加赞数

    思路:点击按钮时调用LikeService中的like方法,对传入的uidtid进行查询,如果发现在like表中没有字段,则执行insert方法。

  • 再次点击按钮减少赞数

    思路:同增加赞数的思想,只是如果发现like表中有字段时才会执行delete方法。

    以下是service层方法。

    @Service
    public class LikeService {
    
        LikeMapper likeMapper;
    
        @Autowired
        public void setLikeMapper(LikeMapper likeMapper) {
            this.likeMapper = likeMapper;
        }
    
        /**
         * add like if the user haven't liked,
         * delete like if the user already have liked
         *
         * @param tid text's id
         * @param uid user's id
         * @return true if the operation is success
         */
        @Transactional(propagation = Propagation.REQUIRED)
        public boolean like(Integer tid, Integer uid) {
            Like like = likeMapper.findLikeByTIdUId(tid, uid);
            if (like == null) {
                return likeMapper.insertLike(tid, uid) == 1;
            } else {
                return likeMapper.deleteLike(tid, uid) == 1;
            }
        }
    }
    

匿名显示

只需要稍微对前端代码进行修改就可以了:

<a href="#" class="badge badge-light" th:text="${!post.anonymous?post.user.username:'anonymous'}"></a>

这里加入一处判断,倘若post是匿名的,则不会显示用户的名字。

5. 好友模块

有了之前的基础,这一模块就相对简单了,主要是实现以下几个特性:

  • 根据ID或用户名查找好友
  • 添加好友
  • 删除好友

好友相关界面一览:

image-20200621121414466

image-20200621115555366

根据ID或用户名查找好友

这一部分主要使用的是userService有关的接口去操作,以下是controller层代码:

@GetMapping("/friends/")
public ModelAndView toMyFriendPage(ModelAndView modelAndView,
                                   @SessionAttribute("loginUser") User user) {
    List<User> friends = friendService.findAllFriendsByUId(user.getId());
    modelAndView.addObject("friends", friends);
    modelAndView.setViewName("friend/myFriend");
    return modelAndView;
}

@GetMapping("/friend/search/")
public ModelAndView findFriend(ModelAndView modelAndView,
                               @Param("findWay") String findWay,
                               @Param("inputSearch") String inputSearch,
                               @SessionAttribute("loginUser") User user) throws Exception {
    List<User> friends = new LinkedList<>();
    if (findWay.equals("id") && !user.getId().equals(Integer.valueOf(inputSearch))) {
        friends.add(userService.findUserById(Integer.valueOf(inputSearch)));
    } else if (findWay.equals("username") && !user.getUsername().equals(inputSearch)) {
        friends = userService.findUserByUsername(inputSearch);
    } else {
        throw new Exception("Wrong with the findWay");
    }
    modelAndView.setViewName("/friend/result");
    modelAndView.addObject("friends", friends);
    return modelAndView;
}

添加好友

@PostMapping("/friend/")
private ModelAndView addFriend(ModelAndView modelAndView,
                               Integer id,
                               @SessionAttribute("loginUser") User user) {
    friendService.addFriend(user.getId(), id);
    List<User> friends = friendService.findAllFriendsByUId(user.getId());
    modelAndView.addObject("friends", friends);
    modelAndView.setViewName("friend/myFriend");
    return modelAndView;
}

可以看到,这里主要是调用了service层的addFriend方法。具体操作便是向friend表单中插入字段。

删除好友

@DeleteMapping("/friend/{id}")
public ModelAndView deleteFriend(ModelAndView modelAndView,
                                 @PathVariable("id") Integer id,
                                 @SessionAttribute("loginUser") User user) throws Exception {

    if (!friendService.deleteFriendById(user.getId(), id)) {
        throw new Exception("the delete operation failed");
    } else {
        List<User> friends = friendService.findAllFriendsByUId(user.getId());
        modelAndView.addObject("friends", friends);
        modelAndView.setViewName("friend/myFriend");
    }
    return modelAndView;
}

这里用了delete方法,倘若搜索不到该用户,该方法会发出异常。

6. 评论模块

评论模块需要实现以下几个功能:

  • 评论的创建删除修改匿名和显示
  • 贴主可以管理评论,可以对评论进行删除操作

效果图:

image-20200621225300015

评论的创建删除修改和匿名

Service层

这里的要点是两个service层的方法:

    @Transactional(propagation = Propagation.REQUIRED)
    public void createComment(Integer pid, Comment comment, User user) {
        comment.setUser(user);
        comment.setTime(new Date(System.currentTimeMillis()));
        textMapper.insertText(comment);
        commentMapper.insertComment(pid);
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public boolean deleteCommentById(Integer id, User user) throws Exception {
        if (commentMapper.findCommentById(id).getUser().getId().equals(user.getId())) {
            return commentMapper.deleteCommentById(id) == 1 &&
                    textMapper.deleteTextById(id) == 1;
        } else {
            throw new Exception("the user do not have right to delete the comment");
        }
    }

createComment在这里扮演的角色是执行comment的创建,这一点和post的创建过程是一样的;

deleteCommentById方法则是和post的删除流程一样。

’仅可删除自己的comment‘功能实现

<div id="commentModule" class="card" style="width: auto">
    <ul class="list-group list-group-flush">
        <li th:each="comment: ${post.comments}" class="list-group-item">
            <div class="row">
                <a href="#" class="badge badge-light card-text"
                   th:text="${!comment.anonymous?comment.user.username:'anonymous'}">Light</a>
                <p class="card-text col-sm-8" th:text="${comment.content}">content</p>
                <form th:action="@{'/comment/' + ${comment.id}}" method="post" th:if="${session.loginUser.id == comment.user.id}">
                    <input type="hidden" name="_method" value="delete"/>
                    <button type="submit" class="btn btn-danger">Delete</button>
                </form>
            </div>
        </li>
    </ul>
</div>

注意到这里其实做了一个判断,如果登录的用户和该comment所属的用户不同的话,将不会出现表单。

同时为了防止恶意提交,在后台Service层也有相应的检查。

    @Transactional(propagation = Propagation.REQUIRED)
    public boolean deleteCommentById(Integer id, User user) throws Exception {
        if (commentMapper.findCommentById(id).getUser().getId().equals(user.getId())) { // 检查该用户是否有权删除
            return commentMapper.deleteCommentById(id) == 1 &&
                    textMapper.deleteTextById(id) == 1;
        } else {
            throw new Exception("the user do not have right to delete the comment");
        }
    }

贴主可以管理评论,可以对评论进行删除操作

效果图

image-20200622111802772

同之前的评论删除相似,这里仅仅是调整了各个评论的删除权限:

<!-- 前端代码调整,调整了按钮出现的逻辑 -->
<form th:action="@{'/comment/' + ${comment.id}}" method="post" th:if="${session.loginUser.id} == ${comment.user.id} or ${session.loginUser.id} == ${comment.text.user.id}">
// 后端service 层删除代码调整
@Transactional(propagation = Propagation.REQUIRED)
public boolean deleteCommentById(Integer id, User user) throws Exception {
    if (commentMapper.findCommentById(id).getUser().getId().equals(user.getId()) || commentMapper.findCommentById(id).getText().getUser().getId().equals(user.getId())) { // 加入了判断该评论的父贴子的用户是否为登录用户的逻辑
        return commentMapper.deleteCommentById(id) == 1 &&
            textMapper.deleteTextById(id) == 1;
    } else {
        throw new Exception("the user do not have right to delete the comment");
    }
}

那么到此为止,一个基础的贴吧就做好了,但其实还和计划时差了一点(没有聊天模块)。随着项目的进行,我也才发现,如果要完成聊天模块的话自己欠缺的知识其实还有许多,那就先欠着吧:-D,等以后闲下来再去学,然后再把这个瘸腿项目给完善了。

另注:该项目已开源,下面是github项目传送门,来一起进步啊~

https://github.com/AIINIRII/postalk