基于Spring Boot的Java教育论坛系统

基于Spring Boot的教学论坛系统

我个人负责的模块

  • 登录登出以及注册的实现
  • 拦截器的实现
  • 论坛活跃度系统以及在排行榜上展示
  • 个人主页的编写
  • 查看我的帖子以及我的回复功能的实现
  • 好帖点赞功能的实现
  • 帖子置顶逻辑的编写
  • 首页帖子展示的编写以及分页逻辑的实现
  • 数据库的设计

具体实现方式

  • 登录登出以及注册

    注册功能的实现

    • 前端

      通过表单向后端发送post请求

      在前端展示两个输入框,第一行是用户名,第二行是密码

      两行分别向后端传递参数

      核心代码如下:

      <form action="/register" method="post">
            <div class="form-group">
              <label for="exampleInputEmail1">用户名</label>
              <input type="text" class="form-control" id="exampleInputEmail1" name="username" placeholder="在此键入用户名">
            </div>
            <div class="form-group">
              <label for="exampleInputPassword1">密码</label>
              <input type="password" class="form-control" id="exampleInputPassword1" name="password" placeholder="在此键入密码">
            </div>
            <button type="submit" class="btn btn-default">注册</button>
            <div class="alert alert-danger col-lg-12 col-md-12 col-sm-12 col-xs-12"
                 th:text="${error}"
                 th:if="${error != null}">
            </div>
      </form>
      
    • 后端

      在接收到前端的传值之后进行判断用户名和密码是否为空

      且判断用户名是否已经存在

      若两者均不为空且用户名未被使用则存入数据库并且写入Cookie

      而Session的写入是在拦截器中进行的,相关代码在拦截器中

      返回首页时就已经是登录状态

      否则注册失败返回异常

      核心代码如下:

      @PostMapping("/register")
          private String doRegister(@RequestParam("username") String username,
                                    @RequestParam("password") String password,
                                    HttpServletResponse response,
                                    Model model){
              model.addAttribute(username);
              model.addAttribute(password);
              if(username==null || username=="")
              {
                  model.addAttribute("error","用户名不能为空");
                  return "register";
              }
              if(password==null || password == "")
              {
                  model.addAttribute("error","密码不能为空");
                  return "register";
              }
              UserExample example = new UserExample();
              example.createCriteria()
                      .andAccountIdEqualTo(username);
              List<User> users = userMapper.selectByExample(example);
              if(users.size() != 0)
              {
                  model.addAttribute("error","用户名已经被注册了");
                  return "register";
              }
              User user =new User();
              user.setAccountId(username);
              user.setPassword(password);
              user.setGmtCreate(System.currentTimeMillis());
              user.setGmtModified(user.getGmtCreate());
              user.setName("小明");
              user.setIsAdmin(false);
              user.setQuestionCount(0);
              user.setCommentCount(0);
              user.setActive(0);
              user.setAvatarUrl("https://pic.imgdb.cn/item/60a9dd9135c5199ba7deab59.jpg");
              String token = UUID.randomUUID().toString();
              user.setToken(token);
              userMapper.insert(user);
              response.addCookie(new Cookie("token",token));
              return "redirect:/";
          }
      

    登录功能的实现

    1. 通过数据库的登录

      • 前端与注册一样,后端也大同小异,因此此处仅展示查询代码

        核心代码如下

            UserExample example = new UserExample();
            example.createCriteria()
                .andAccountIdEqualTo(username)
                .andPasswordEqualTo(password);
            List<User> users = userMapper.selectByExample(example);
            if(users.size() == 0)
            {
                model.addAttribute("error","用户名或密码错误");
                return "login";
            }
            User dbUser = users.get(0);
            User upadteUser = new User();
            upadteUser.setGmtModified(System.currentTimeMillis());
            String token = UUID.randomUUID().toString();
            upadteUser.setToken(token);
            UserExample userExample = new UserExample();
            userExample.createCriteria()
                .andIdEqualTo(dbUser.getId());
            userMapper.updateByExampleSelective(upadteUser, userExample);
            response.addCookie(new Cookie("token",token));
            return "redirect:/";
        
    2. 连接Gitee通过OAuth2认证登录

      • OAuth2认证的基本流程

        OAuth2认证流程图

        对于此认证Gitee也有官方文档,如果看不懂我接下来的解释可以去看官方文档[Gitee对于OAuth2的官方文档](Gitee OAuth 文档)

        上图中的应用服务器可以理解为运行程序的电脑

        浏览器就是打开本论坛所使用的浏览器

        当你在Gitee中申请了可以接入Gitee的应用之后你会获得一个应用ID以及你自己填写的回调地址

        你用你手上的这两样东西可以拼成以下这个形式的链接

        https://gitee.com/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code
        

        其中client_id就是应用id,redirect_uri就是回调地址,千万记得大括号也要一并删除(不要和我一样可爱)

        当用户在这个链接中登录自己的Gitee账号后,我们就走完了图中的AB路线

        此时Gitee会通过客户端也就是浏览器给我们传回一个用户授权码(这个授权码会返回在url中我们在控制层中用@RequestParam方法进行接收就可以了!)

        当我们得到授权码之后我们又可以和另外两样东西拼成下面这个形式的链接

        https://gitee.com/oauth/token?grant_type=authorization_code&code={code}&client_id={client_id}&redirect_uri={redirect_uri}&client_secret={client_secret}
        

        其中code就是我们刚刚得到的用户授权码

        之后Gitee就会返回一串String

        输出String并观察access_token的位置就可以把它截取出来并通过其获取用户信息了!

        代码实现逻辑:

        /*
        从这里进行获取code并初始化accessTkoenDTO
        并且用一个随机的UUID作为Cookie
        */
        public String callback(@RequestParam(name = "code") String code,
                                   HttpServletResponse response){
                AccessTokenDTO accessTokenDTO = new AccessTokenDTO();
                accessTokenDTO.setClient_id(clientId);
                accessTokenDTO.setClient_secret(clientSecert);
                accessTokenDTO.setCode(code);
                accessTokenDTO.setRedirect_uri(redircrUri);
                String accessToken = giteeProvider.getAccessToken(accessTokenDTO);
                GiteeUser giteeUser = giteeProvider.getUser(accessToken);
                if(giteeUser!=null && giteeUser.getId()!=0)
                {
                    //登录成功,写cookie和session
                    User user = new User();
                    //User user1 ;
                    String token = UUID.randomUUID().toString();
                    user.setToken(token);
                    user.setName(giteeUser.getName());
                    user.setAccountId(String.valueOf(giteeUser.getId()));
                    user.setAvatarUrl(giteeUser.getAvatarUrl());
        
                    //user1 = userMapper.findByAccountId(user.getAccountId());
                    /*if(user1==null)*/
                    userService.createOrUpdate(user);
                    response.addCookie(new Cookie("token",token));
                    return "redirect:/";
                }
         }
        
        /*获取accesstoken的具体逻辑以及通过其获取用户信息的方法*/
        @Component
        public class GiteeProvider {
            public String getAccessToken(AccessTokenDTO accessTokenDTO){
                MediaType mediaType = MediaType.get("application/json; charset=utf-8");
        
                OkHttpClient client = new OkHttpClient();
        
                RequestBody body = RequestBody.create(mediaType, JSON.toJSONString(accessTokenDTO));
                Request request = new Request.Builder()
                        .url("https://gitee.com/oauth/token?grant_type=authorization_code&code="+accessTokenDTO.getCode()+"&client_id="+accessTokenDTO.getClient_id()+"&redirect_uri="+accessTokenDTO.getRedirect_uri()+"&client_secret="+accessTokenDTO.getClient_secret())
                        .post(body)
                        .build();
                try (Response response = client.newCall(request).execute()) {
                    String string = response.body().string();
                    String token=null;
                    int cnt=0,l=0;
                    boolean flag=false;
                    for(int i=0;i<string.length();i++)
                    {
                        if(string.charAt(i)=='\"') {
                            cnt++;
                        }
                        if(cnt==3&&!flag)
                        {
                            l=i+1;
                            flag=true;
                        }
                        if(cnt==4)
                        {
                            token=string.substring(l,i);
                            break;
                        }
                    }
                    //System.out.println(string);
                    //System.out.println(token);
                    return token;
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return null;
            }
            public GiteeUser getUser(String accessToken){
                OkHttpClient client = new OkHttpClient();
                Request request = new Request.Builder()
                        .url("https://gitee.com/api/v5/user?access_token=" +accessToken)
                        .build();
                try {
                    Response response = client.newCall(request).execute();
                    String string=response.body().string();
                    GiteeUser giteeUser=JSON.parseObject(string,GiteeUser.class);
                    return giteeUser;
                } catch (IOException e) {
                }
                return null;
            }
        }
        
      • 前端

        前端比较简单,只需要一个可以点进去的链接作为入口就可以了,这里不做展示

    登出功能的实现

    • 基本思路

      实现用户的登出要把存在服务端的Session和存在客户端的Cookie都一并清除

      清除Session好说,有现成的语法可以用,如下:

      request.getSession().removeAttribute("这里是你的session名");
      

      而清除Cookie会稍微麻烦一点,首先用一个同名的空cookie把原来的cookie覆盖掉,之后把它的存在时间改为0,他就会消失,具体代码如下:

          Cookie cookie = new Cookie("token",null);
          cookie.setMaxAge(0);
          response.addCookie(cookie);
      
    • 前端

      一样只需要一个入口,在此不多展示

  • 拦截器

    • 定义一个拦截器

      首先我们需要知道我们这个拦截器的作用是什么,我要写的拦截器是在用户进入这个页面之后通过查看客户端浏览器的Cookie来之间确定是否为登录态

      很显然我们的拦截器是要在执行Controller之前就要执行的

      而HandlerInterceptor为我们提供了三种重写方法,分别是:preHandle,postHandle,afterCompletion

      其中preHandle方法是符合我们要求的,所有我们只需要重写它就可以了

      我们通过枚举cookie来找到名为token的cookie,在利用数据库的查询语句查找是否有对应的用户

      如果有,则写入session

      核心代码如下:

      //其中usermapper是通过mybatis逆向生成的文件
      @Override
          public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
              Cookie[] cookies = request.getCookies();
              if(cookies!=null&&cookies.length!=0) {
                  for(Cookie cookie:cookies)
                  {
                      if(cookie.getName().equals("token"))
                      {
                          String token=cookie.getValue();
                          UserExample example = new UserExample();
                          example.createCriteria()
                                  .andTokenEqualTo(token);
                          List<User> users = userMapper.selectByExample(example);
                          if(users.size()!=0)
                          {
                              request.getSession().setAttribute("user",users.get(0));
                          }
                          break;
                      }
                  }
              }
              return true;
          }
      
    • 添加拦截器

      我们要在每个页面都判断一下是否为登录态,所有配置如下:

      @Configuration
      public class WebConfig implements WebMvcConfigurer {
          @Autowired
          private SessionInterceptor sessionInterceptor;
      
          @Override
          public void addInterceptors(InterceptorRegistry registry) {
              registry.addInterceptor(sessionInterceptor).addPathPatterns("/**");
          }
      }
      
  • 论坛活跃度系统以及在排行榜上的展示

    • 活跃度设置

      由于不是很会怎么设置比较高级的活跃度设置,就设置为发一次贴增加5点活跃度,回复一次增加3点活跃度(经验+3!)

    • 需要考虑的细节

      注:由于博主能力有限以及时间过紧,本功能没有处理高并发的能力

      由于发布帖子界面同时兼顾修改帖子的功能,所有要首先判断帖子id是否为空再进行活跃度的计算,如下:

          if(question.getId()==null)
          {
              user.setQuestionCount(user.getQuestionCount()+1);
              user.setActive(user.getActive()+5);
              userMapper.updateByPrimaryKey(user);
          }
      

      在删除帖子时相应的活跃度也需要减少,通过枚举评论来减少活跃度,要注意在delete语句之前执行,如下:

       @GetMapping("/delete/{id}")
          public String delete(@PathVariable(name = "id") String id,
                               Model model){
      
              QuestionExample questionExample = new QuestionExample();
              questionExample.createCriteria()
                      .andIdEqualTo(Integer.parseInt(id));
              Question question = questionMapper.selectByPrimaryKey(Integer.parseInt(id));
              User user = userMapper.selectByPrimaryKey(question.getCreator());
              user.setQuestionCount(user.getQuestionCount()-1);
              user.setActive(user.getActive()-5);
              userMapper.updateByPrimaryKey(user);
              questionMapper.deleteByExample(questionExample);
      
              CommentExample commentExample = new CommentExample();
              commentExample.createCriteria()
                      .andParentIdEqualTo(Integer.parseInt(id));
              int count = (int) commentMapper.countByExample(commentExample);
              List<Comment> comments = commentMapper.selectByExample(commentExample);
              for(Comment comment:comments)
              {
                  user = userMapper.selectByPrimaryKey(comment.getCommentator());
                  user.setActive(user.getActive()-3);
                  user.setCommentCount(user.getCommentCount()-1);
                  userMapper.updateByPrimaryKey(user);
              }
              commentMapper.deleteByExample(commentExample);
              return "delete";
          }
      
    • 排行榜的展示

      通过数据库排序语句进行排序,前端通过th:each语句进行枚举

      前端核心代码:

      <table class="table table-striped">
          <thead>
              <tr>
                <th>排名</th>
                <th>用户名</th>
                <th>发帖数</th>
                <th>回帖数</th>
                <th>活跃度</th>
              </tr>
          </thead>
          <tbody>
              <tr th:each="user : ${users}">
                <th th:text="${user.rank}"></th>
                <th th:text="${user.name}"></th>
                <th th:text="${user.questionCount}"></th>
                <th th:text="${user.commentCount}"></th>
                <th th:text="${user.active}"></th>
              </tr>
          </tbody>
      </table>
      

      后端核心代码:

      @RequestMapping("/rank/{type}")
          private String rank(@PathVariable(name = "type") Integer type,
                              Model model){
              List<User> users = new ArrayList<>();
              List<UserDTO> userDTOS = new ArrayList<>();
              if(type==1){
                  UserExample example = new UserExample();
                  example.createCriteria();
                  example.setOrderByClause("active DESC");
                  users = userMapper.selectByExample(example);
              }else if(type==2){
                  UserExample example = new UserExample();
                  example.setOrderByClause("question_count DESC");
                  users = userMapper.selectByExample(example);
              }else {
                  UserExample example = new UserExample();
                  example.setOrderByClause("comment_count DESC");
                  users = userMapper.selectByExample(example);
              }
      
              for(int i=0;i<users.size();i++)
              {
                  UserDTO u=new UserDTO();
                  BeanUtils.copyProperties(users.get(i),u);
                  userDTOS.add(u);
                  userDTOS.get(i).setRank(i+1);
              }
              model.addAttribute("users",userDTOS);
              return "rank";
          }
      
  • 个人页面的编写

    在个人页面中展示个人头像在右侧表单跳转到我的帖子以及我的回复

    在表单上的徽章代表帖子和回复的个数

    • 前端核心代码

      <div class="list-group">
          <a th:href="@{'/profile/'+${session.user.id}+'/questions'}" class="list-group-item">
              <span class="badge" th:text="${questionNumber}"></span>
              我的帖子
          </a>
          <a th:href="@{'/profile/'+${session.user.id}+'/replies'}" class="list-group-item">
              <span class="badge" th:text="${commentNumber}"></span>
              我的回帖
          </a>
      </div>
      
    • 后端核心代码

      @RequestMapping("/profile/{id}")
          private String profile(Model model,
                                 @RequestParam(name="page",defaultValue = "1") Integer page,
                                 @RequestParam(name = "size",defaultValue = "5") Integer size,
                                 @PathVariable(name = "id") Integer id){
              PagingDTO pagination= questionService.list(page,size);
              model.addAttribute("pagination",pagination);
              User user = userMapper.selectByPrimaryKey(id);
              QuestionExample example = new QuestionExample();
              example.createCriteria()
                      .andCreatorEqualTo(id);
              Integer questionNumber = (int) questionMapper.countByExample(example);
              CommentExample example1 = new CommentExample();
              example1.createCriteria()
                      .andCommentatorEqualTo(id);
              Integer commentNumber = (int) commentMapper.countByExample(example1);
              model.addAttribute("questionNumber",questionNumber);
              model.addAttribute("commentNumber",commentNumber);
              model.addAttribute("user",user);
              model.addAttribute("id",id.toString());
              return "profile";
          }
      
  • 我的问题以及我的回复

    通过路径上的id来判断用户,使用数据库语句进行查询,使用分页逻辑进行展示

    • 前端核心代码:

       <div class="media" th:if="${section}=='questions'" th:each="question : ${pagination.questions}">
      	<div class="media-left">
          	<a href="#">
              	<img class="media-object img-rounded"th:src="${question.user.avatarUrl}">
              </a>
          </div>
          <div class="media-body">
          	<h4 class="media-heading" th:text="${question.title}"></h4>
              <span th:text="${question.description}"></span>
              <br>
              <span class="text-desc">
                  <span th:text="${question.commentCount}"></span> 个回复 • 
                  <span th:text="${question.viewCount}"></span> 次浏览 • 
                  <span th:text="${#dates.format(question.gmtCreate,'yyyy-MM-dd HH:mm')}"></span>
              </span>
          </div>
      </div>
      <div class="media" th:if="${section}=='replies'" th:each="comment : ${pagination.comments}">
      	<div class="media-left">
      		<a href="#">
      			<img class="media-object img-rounded"th:src="${comment.user.avatarUrl}">
              </a>
          </div>
          <div class="media-body">
          <h4 class="media-heading" th:text="${comment.content}"></h4>
          <span th:text="${comment.question.title}"></span> <br>
          <span class="text-desc"><span th:text="${comment.likeCount}"></span> 个点赞</span></span>
          </div>
      </div>
      
    • 后端核心代码

      @GetMapping("/profile/{id}/{action}")
          public String profile(@PathVariable(name = "action") String action,
                                @PathVariable(name = "id") Integer id,
                                HttpServletRequest request,
                                Model model,
                                @RequestParam(name="page",defaultValue = "1") Integer page,
                                @RequestParam(name = "size",defaultValue = "5") Integer size){
              User user = (User) request.getSession().getAttribute("user");
              if(user==null){
                  return "redirect:/";
              }
              if("questions".equals(action)){
                  model.addAttribute("section","questions");
                  model.addAttribute("sectionName","我的帖子");
                  PagingDTO paginationDTO = questionService.list(id, page, size);
                  model.addAttribute("pagination",paginationDTO);
              }else if("replies".equals(action)){
                  model.addAttribute("section","replies");
                  model.addAttribute("sectionName","我的回复");
                  PagingDTO pagingDTO = commentService.list(id,page,size);
                  model.addAttribute("pagination",pagingDTO);
              }
              model.addAttribute("id",id.toString());
              return "profileMy";
          }
      
  • 好帖点赞功能的实现

    • 实现方式

      我能想到的只有最原始最暴力的方法,新建一张表

      parent_id表示被点赞的对象,可以是帖子的id也可以是回复的id

      type表示被点赞对象,若值为1则表示为对帖子的点赞,若值为2则表示对回复的点赞

      liker_id表示点赞者的id

      当用户点击点赞按钮时,在数据库中通过上面三项进行查找匹配

      若为空则点赞成功,否则点赞失败

    • 核心代码:

      @RequestMapping("/like/{id}/{type}")
          private String like(@PathVariable(name = "id") Integer id,
                              @PathVariable(name = "type") Integer type,
                              HttpServletRequest request,
                              Model model){
      
              User user = (User) request.getSession().getAttribute("user");
      
      
              LoveExample example = new LoveExample();
      
              example.createCriteria()
                      .andParentIdEqualTo(id)
                      .andTypeEqualTo(type)
                      .andLikerIdEqualTo(user.getId());
              List<Love> loves = loveMapper.selectByExample(example);
              if(loves.size()==0){
                  Love love = new Love();
                  love.setIsLike(true);
                  love.setLikerId(user.getId());
                  love.setParentId(id);
                  love.setType(type);
                  Question question = new Question();
                  if(type.equals(1)){
                      question = questionMapper.selectByPrimaryKey(id);
                      user = userMapper.selectByPrimaryKey(question.getCreator());
                      question.setLikeCount(question.getLikeCount()+1);
                      question.setGmtModified(System.currentTimeMillis());
                      questionMapper.updateByPrimaryKey(question);
                      List<CommentDTO> commentDTOList = commentService.getByParentIdList(id);
                      model.addAttribute("comments",commentDTOList);
                  }else{
                      Comment comment = commentMapper.selectByPrimaryKey(id);
                      question = questionMapper.selectByPrimaryKey(comment.getParentId());
                      user = userMapper.selectByPrimaryKey(question.getCreator());
                      comment.setLikeCount(comment.getLikeCount()+1);
                      comment.setGmtModified(System.currentTimeMillis());
                      commentMapper.updateByPrimaryKey(comment);
                      List<CommentDTO> commentDTOList = commentService.getByParentIdList(comment.getParentId());
                      model.addAttribute("comments",commentDTOList);
                  }
                  QuestionDTO questionDTO =new QuestionDTO();
                  BeanUtils.copyProperties(question,questionDTO);
                  questionDTO.setUser(user);
                  model.addAttribute("question",questionDTO);
                  loveMapper.insert(love);
                  return "question";
              }else {
                  Question question = new Question();
                  if(type.equals(1)){
                      question = questionMapper.selectByPrimaryKey(id);
                      user = userMapper.selectByPrimaryKey(question.getCreator());
                      List<CommentDTO> commentDTOList = commentService.getByParentIdList(id);
                      model.addAttribute("comments",commentDTOList);
                  }else{
                      Comment comment = commentMapper.selectByPrimaryKey(id);
                      question = questionMapper.selectByPrimaryKey(comment.getParentId());
                      user = userMapper.selectByPrimaryKey(question.getCreator());
                      List<CommentDTO> commentDTOList = commentService.getByParentIdList(comment.getParentId());
                      model.addAttribute("comments",commentDTOList);
                  }
                  QuestionDTO questionDTO =new QuestionDTO();
                  BeanUtils.copyProperties(question,questionDTO);
                  questionDTO.setUser(user);
                  model.addAttribute("question",questionDTO);
                  model.addAttribute("error", "已经赞过了");
                  return "question";
              }
          }
      
  • 帖子置顶逻辑的编写

    • 实现方式

      直接给帖子一个bool标签,之后通过数据库语句暴搜(蓝桥杯骗分技巧)

      在前端先枚举每一个被置顶的帖子,再在后面用分页功能实现对剩下帖子的枚举

    • 前端核心代码

      <blockquote th:each="top : ${tops}">
      	<div class="media" >
              <div class="media-left">
                  <a th:href="@{'/profile/'+${top.user.id}}">
                      <img class="media-object img-rounded"
                           style="width: 60px;height: 60px;"
                           th:src="${top.user.avatarUrl}">
                  </a>
              </div>
              <div class="media-body">
                  <h4 class="media-heading" >
                      <svg  class="icon" aria-hidden="true">
                          <use xlink:href="#icon-zhiding"></use>
                      </svg>
                      <a th:href="@{'/question/'+${top.id}}" th:text="${top.title}"></a>
                  </h4>
                  <span th:text="${top.description}"></span> <br>
                  <span class="text-desc">
                      <span th:text="${top.commentCount}"></span> 个回复 • 
                      <span th:text="${top.viewCount}"></span> 次浏览 • 
                      <span th:text="${#dates.format(top.gmtCreate,'yyyy-MM-dd HH:mm')}"></span>
                  </span>
              </div>
          </div>
      </blockquote>
      
    • 后端核心代码

      @RequestMapping("/")
          public String index(Model model,
                              @RequestParam(name="page",defaultValue = "1") Integer page,
                              @RequestParam(name = "size",defaultValue = "7") Integer size,
                              @RequestParam(name ="search",required = false) String search){
      
              List<QuestionDTO> top = new ArrayList<>();
              List<Question> tops =new ArrayList<>();
      
              if(!StringUtils.isNullOrEmpty(search)){
                  PagingDTO pagination= questionService.list(page,size,search);
                  model.addAttribute("pagination",pagination);
                  return "index";
              }else {
                  QuestionExample example = new QuestionExample();
                  example.createCriteria()
                          .andIsTopEqualTo(true);
                  tops = questionMapper.selectByExampleWithBLOBs(example);
                  PagingDTO pagination= questionService.list(page,size);
                  model.addAttribute("pagination",pagination);
              }
              for (Question question : tops) {
                  QuestionDTO questionDTO = new QuestionDTO();
                  User user = userMapper.selectByPrimaryKey(question.getCreator());
                  BeanUtils.copyProperties(question, questionDTO);
                  questionDTO.setUser(user);
                  questionDTO.setCommentCount(commentService.countByParentId(question.getId()));
                  top.add(questionDTO);
              }
              model.addAttribute("tops",top);
              return "index";
          }
      
  • 首页的编写以及分页逻辑的实现

    • 实现方式

      对于首页的编写由于导航栏每个页面都需要所有我把它封装起来了

      而首页只需要展示置顶的帖子和由新到旧的帖子展示就可以了

      都比较好实现,只不过要注意mybatis中排序语法的使用,如下

      example.setOrderByClause("gmt_create DESC");//DESC为降序排列,默认为升序,前边为表中的元素
      

      分页逻辑就是一个简单的模拟,首先读入页码,以此为中间值来插入左右的页码

      通过此是否为第一页来判断是否有上一页按钮

      通过页码栏中是否含有第一页来判断是否有首页按钮

      判断下一页和尾页逻辑一致

      而对于每一页展示哪个页面mybatis逆向生成的语句中有有个可以设置偏移量和查询个数的语句

      使用方法如下:

      List<Question> questions = questionMapper
          				.selectByExampleWithBLOBsWithRowbounds(example, new RowBounds(offset, size));
      

      其中offset为偏移量,很简单可以推出为offset=size*(page-1)

      size为一页展示几条帖子,我设置默认为5

      理解了这些之后就很好实现了

    • 部分核心代码

      Controller中传值代码如下:

      PagingDTO pagination= questionService.list(page,size);
      model.addAttribute("pagination",pagination);
      

      QuestionService中对应的list方法如下:

       public PagingDTO list(Integer page, Integer size) {
              PagingDTO paginationDTO = new PagingDTO();
              Integer totalPage;
              QuestionExample example = new QuestionExample();
              example.createCriteria()
                      .andIsTopEqualTo(false);
              Integer totalCount=(int)questionMapper.countByExample(example);
              if(totalCount%size==0)
              {
                  totalPage=totalCount/size;
              }
              else
              {
                  totalPage=totalCount/size+1;
              }
      
              if(page<1) {
                  page=1;
              }
              if(page>totalPage) {
                  page=totalPage;
              }
      
              paginationDTO.setPagination(totalPage,page);
      
              Integer offset = size * (page - 1);
              example.setOrderByClause("gmt_create DESC");
              List<Question> questions = questionMapper.selectByExampleWithBLOBsWithRowbounds(example, new RowBounds(offset, size));
              List<QuestionDTO> questionDTOList=new ArrayList<>();
      
              for(Question question:questions)
              {
                  User user = userMapper.selectByPrimaryKey(question.getCreator());
                  QuestionDTO questionDTO = new QuestionDTO();
                  questionDTO.setId(question.getId());
                  BeanUtils.copyProperties(question,questionDTO);
                  questionDTO.setCommentCount(commentService.countByParentId(question.getId()));
                  questionDTO.setUser(user);
                  questionDTOList.add(questionDTO);
              }
              paginationDTO.setQuestions(questionDTOList);
              return paginationDTO;
          }
      

      PagingDTO 中的参数设置如下:

      @Data
      public class PagingDTO {
          private List<QuestionDTO> questions;
          private List<CommentDTO> comments;
          private boolean showPrevious;
          private boolean showFirstPage;
          private boolean showNext;
          private boolean showEndPage;
          private Integer page;
          private List<Integer> pages = new ArrayList<>();
          Integer totalPage;
      
          public void setPagination(Integer totalPage, Integer page) {
              this.totalPage=totalPage;
              this.page=page;
              pages.add(page);
              for(int i=1;i<=3;i++)
              {
                  if(page-i>0){
                      pages.add(0,page-i);
                  }
                  if(page+i<=totalPage){
                      pages.add(page+i);
                  }
              }
      
              if(page==1){
                  showPrevious=false;
              }
              else{
                  showPrevious=true;
              }
              if(page.equals(totalPage)){
                  showNext=false;
              }
              else{
                  showNext=true;
              }
              if(pages.contains(1)){
                  showFirstPage=false;
              }else {
                  showFirstPage=true;
              }
              if(pages.contains(totalPage)){
                  showEndPage=false;
              }else {
                  showEndPage=true;
              }
          }
      }
      

      注:在此DTO中没有get,set方法是因为使用了Lombok中的@Data方法

  • 数据库的设计

    • 设计思路

      用户,问题,评论需要各有一张表来存储信息

      而点赞在经过了思考之后也决定加一张表,所以本工程用到的表如下:

      user表:

      表的元素 元素的用途
      id 主键,唯一,自增
      account_id 用来记录用户名
      password 用来记录用户密码
      name 用来记录用户的昵称
      token 存储对应用户Cookie中储存的值
      is_admin 用来记录该用户是否为管理员
      avatar_url 用来存储头像的地址
      question_count 用来储存用户的帖子数
      comment_count 用来储存用户的回复数
      active 用来储存用户的活跃度
      gmt_create 创建用户的时间戳
      gmt_modified 修改用户的时间戳

      question表:

      表的元素 元素的作用
      id 主键,唯一,自增
      title 用来储存帖子的标题
      description 用来储存帖子的描述
      creator 用来储存发帖人的id
      comment_count 用来储存该帖子的回复数
      view_count 用来储存该帖子的浏览数
      tag 用来储存该帖子的标签
      is_top 用来储存该帖子是否被置顶
      like_count 用来储存该帖子的获赞数
      gmt_create 创建帖子的时间戳
      gmt_modified 修改帖子的时间戳

      comment表

      表的元素 元素的作用
      id 主键,唯一,递增
      parent_id 用来储存被评论帖子的id
      commentator 用来储存评论人的id
      like_count 用来储存该评论的点赞数
      content 用来储存该评论的内容
      gmt_create 用来储存该评论创建的时间戳
      gmt_modified 用来储存该评论修改的时间戳

      love表

      表的元素 元素的作用
      id 主键,唯一,递增
      parent_id 用来储存被点赞的帖子或者回复的id
      type 用来储存被点赞的是帖子还是回复
      liker_id 用来储存点赞者的id
      is_like 用来储存该点赞者是否已经点过赞

不足与展望改进

还有很多地方需要改进,还有许多功能不够完善,基本没有处理高并发的能力,希望后续可以完善其功能。

posted @ 2021-06-22 17:27  TheWeak  阅读(516)  评论(0编辑  收藏  举报