1.5 社区开发第(一)弹:开发社区首页

124 阅读5分钟

6. 开发社区首页

image-20220704160449355.png

浏览器在给服务器发送请求时,首先发送给 Controller 然后 Controller 调 Service,然后 Service 调 DAO,所以我们开发的时候建议先开发 DAO,然后开发 Service,最后开发 controller。

1. 开发DAO

社区首页需要用到的数据库表

CREATE TABLE `discuss_post` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(45) DEFAULT NULL,
  `title` varchar(100) DEFAULT NULL,
  `content` text,
  `type` int(11) DEFAULT NULL COMMENT '0-普通; 1-置顶;',
  `status` int(11) DEFAULT NULL COMMENT '0-正常; 1-精华; 2-拉黑;',
  `create_time` timestamp NULL DEFAULT NULL,
  `comment_count` int(11) DEFAULT NULL,
  `score` double DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `index_user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=281 DEFAULT CHARSET=utf8;

image-20220704161016924.png

本次开发用到的类在下面图中显示

image-20220705130814494.png

首先需要建立一个与该表对应的实体类DiscussPost

public class DiscussPost {

    private int id;
    private int userId;
    private String title;           // 标题
    private String content;         // 内容
    private int type;               // 帖子类型
    private int status;             // 帖子状态
    private Date createTime;       // 创建时间
    private int commentCount;      // 评论数量
    private double score;              // 分数/热度
	
  	// get、set、toString方法为了以免影响阅读就没粘,实际开发时是有的
}

接下来开发DAO接口:DiscussPostMapper

@Mapper
public interface DiscussPostMapper {

    List<DiscussPost> selectDiscussPosts(int userId, int offset, int limit);      // 分页查询
    /*
    实现功能:分页查询
    参数1:userId   参数2:从第几条数据开始展示   参数3:每页展示几条数据
    这里要说一下,这个功能之一是展示首页的帖子,其实我们只需要查询所有数据然后分页就好了不需要传 userId,
    但是以后我们开发的时候有一个我的主页的功能,里面显示的都是我们自己的帖子,我们可以通过
    传 userId 展示自己发布过的所有帖子,所以为了以后开发方便,还是建议加上 userId
    具体实现展示首页帖子时 userId 传0, mapper配置文件处会进行修改,userId为0时查询所有帖子
     */

    int selectDiscussPostRows(@Param("userId") int userId);
    /*
    实现功能:查询数据库中有多少条数据
    参数1:userId
    在展示社区首页的时候,有一个页码,总共有多少页是由总数据条数和每页显示多少条数据决定的,我们可以把每页显示的
    数据条数固化下来,然后我们只需要一个方法查询一下数据库中总共有多少条数据。
    同样在查询社区首页数据时userId传入0,在mapper配置文件中处理时直接查询所有
    补充:@Param 注解用于给参数起别名,在mapper配置文件的动态sql中可以使用这个别名
         如果某个方法只有一个参数,并且在<if>里(动态sql)使用,则必须用@Param注解给这个参数起别名

     */

}
# 补充:@Param注解
这个注解是给这个参数起一个别名,在mapper配置文件中写sql时就可以用这个别名,
另外:如果mapper配置文件中需要用到动态sql并且需要用到这个参数,并且这个方法
只有一个参数,这个时候,这个参数一定要加上@Param注解起别名,否则会报错。

接下来我们要创建这个DAO接口对应的mapper配置文件:discusspost-mapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nowcoder.community.dao.DiscussPostMapper">

    <sql id="selectFields" >
        id, user_id, title, content, type, status, create_time, comment_count, score
    </sql>
    <select id="selectDiscussPosts" resultType="com.nowcoder.community.entity.DiscussPost">
        select <include refid="selectFields"></include>
        from discuss_post
        where status != 2
        <if test="userId!=0">
            and user_id = #{userId}
        </if>
        order by type desc, create_time desc
        limit #{offset}, #{limit}
    </select>

    <select id="selectDiscussPostRows" resultType="Integer">
        select count(id)
        from discuss_post
        where status != 2
        <if test="userId!=0">
            and user_id = #{userId}
        </if>
    </select>

</mapper>
  • 拉黑帖子不能展示(status = 2 表示拉黑帖子)

image-20220704211725052.png

因为mapper配置文件中的sql很容易写错,所以开发完dao之后建议在测试类中测试一下dao接口是否开发成功。

接下来开发业务层service

DiscussPostService:

@Service
public class DiscussPostService {

    @Autowired
    private DiscussPostMapper discussPostMapper;

    public List<DiscussPost> findDiscussPosts(int userId, int offest, int limit){
        return discussPostMapper.selectDiscussPosts(userId, offest, limit);
    }

    public int findDiscussPostRows(int userId){
        return discussPostMapper.selectDiscussPostRows(userId);
    }

}
我们查到的是userId,但是社区首页显示的肯定不是userId,而是user的用户名,我们可以
直接在mapper配置文件中的sql进行关联查询,也可以在得到DiscussPost后根据userId单独
地查一下user的用户名,把查到的user和DiscussPost组合到一起返回给页面。这里我们采用
第二种,虽然第二种看起来比较麻烦,但是未来使用redis缓存一些数据的时候会比较方便,性能
比较高。所以我们就需要一种方法根据userId到user,那这个方法写到DiscussPostService就
不合适了,因为是跟user有关的操作,所以我们需要新建一个service即UserService,那
UserService肯定需要用到UserDAO接口,但是这个接口已经写过了,所以我们只需要写UserService
就可以了。

UserService:

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public User findUserById(int id){
        return userMapper.selectById(id);
    }
}

接下来将需要用到的一些静态资源、模板文件粘贴到项目中去

image-20220704225333751.png

接下来我们来开发视图层,首先来开发视图层:

注:如果controller类上不加访问路径,那访问的时候就不需要写类的路径了,直接写方法的路径就可以了

@Controller
public class HomeController {

    @Autowired
    private DiscussPostService discussPostService;

    @Autowired
    private UserService userService;

    @RequestMapping(path = "/index", method = RequestMethod.GET)
    public String getIndexPage(Model model){
        List<DiscussPost> list = discussPostService.findDiscussPosts(0, 0, 10);
        List<Map<String, Object>> discussPosts = new ArrayList<>();
        if(list != null){
            for(DiscussPost post : list){
                Map<String, Object> map = new HashMap<>();
                map.put("post", post);
                User user = userService.findUserById(post.getUserId());
                map.put("user", user);
                discussPosts.add(map);
            }
        }
        model.addAttribute("discussPosts", discussPosts);
        return "/index";            // 返回templates目录下的index.html
    }

}

image-20220705093836320.png

接下来就是修改与这个controller相关的thymeleaf模板了 index.html

# 注:将html改成themeleaf模板一定要修改第二行为
<html lang="en" xmlns:th="http://www.thymeleaf.org">

与帖子相关的themeleaf模板的部分内容

<!-- 帖子列表 -->
<ul class="list-unstyled">
   <li class="media pb-3 pt-3 mb-3 border-bottom" th:each="map:${discussPosts}">
      <a href="site/profile.html">
         <img th:src="${map.user.headerUrl}" class="mr-4 rounded-circle" alt="用户头像" style="width:50px;height:50px;">
      </a>
      <div class="media-body">
         <h6 class="mt-0 mb-3">
            <a href="#" th:utext="${map.post.title}">备战春招,面试刷题跟他复习,一个月全搞定!</a>
            <span class="badge badge-secondary bg-primary" th:if="${map.post.type==1}">置顶</span>
            <span class="badge badge-secondary bg-danger" th:if="${map.post.status==1}">精华</span>
         </h6>
         <div class="text-muted font-size-12">
            <u class="mr-3" th:text="${map.user.username}">寒江雪</u> 发布于 <b th:text="${#dates.format(map.post.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-15 15:32:18</b>
            <ul class="d-inline float-right">
               <li class="d-inline ml-2">赞 11</li>
               <li class="d-inline ml-2">|</li>
               <li class="d-inline ml-2">回帖 7</li>
            </ul>
         </div>
      </div>                
   </li>
</ul>
# 小知识补充:
th:text     里面的内容会原样输出
th:utext    里面如果有转义字符会转义后再输出

image-20220705082952846.png

image-20220705083113349.png

image-20220705095238742.png

实际效果:

image-20220705095732141.png

接下来我们来开发一下分页组件:

分页对应的实体类:Page

public class Page {

    // 当前页码
    private int current = 1;        // 默认为1
    // 每页显示的条数
    private int limit = 10;         // 默认为10
    // 从数据库中查询数据总数(用于计算总页数)
    private int rows;
    // 查询路径(用于复用分页链接)
    private String path;

    public int getCurrent() {
        return current;
    }

    public void setCurrent(int current) {
        if(current >= 1){
            this.current = current;
        }
    }

    public int getLimit() {
        return limit;
    }

    public void setLimit(int limit) {
        if(limit >= 1 && limit <= 100){
            this.limit = limit;
        }
    }

    public int getRows() {
        return rows;
    }

    public void setRows(int rows) {
        if(rows >= 0){
            this.rows = rows;
        }
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    /**
     * 获取当前页的起始行
     * 页面点击页码跳转到下一页,数据库查询的时候并不是通过当前页查询,
     * 而是根据当前页的起始行查询,所以我们需要通过当前页的页面算出
     * 当前页的起始行
     * @return
     */
    public int getOffset(){
        // current * limit - limit
        return (current - 1) * limit;
    }

    /**
     * 获取总页数
     * 这个方法是为了页面显示页码时左边界判断需要的条件
     * @return
     */
    public int getTotal(){
        // rows / limit [+1]
        if(rows % limit == 0){
            return rows / limit;
        } else {
            return rows / limit + 1;
        }
    }

    /**
     * 获取起始页码
     * 这个起始页码是当前页码附近的页码,左边的页码
     * 起始页码和中止页码会显示,中间的都用.省略
     * @return
     */
    public int getFrom(){
        int from = current - 2;
        return from < 1 ? 1 : from;
    }

    /**
     * 获取结束页码
     * @return
     */
    public int getTo(){
        int to = current + 2;
        int total = getTotal();
        return to > total ? total : to;
    }


}

另外,我们还需要将前面的HomeController中的查询方法修改一下(参数另加了分页类):

@Controller
public class HomeController {

    @Autowired
    private DiscussPostService discussPostService;

    @Autowired
    private UserService userService;

    @RequestMapping(path = "/index", method = RequestMethod.GET)
    public String getIndexPage(Model model, Page page){
        // 方法调用前,SpringMVC会自动实例化Model和Page,并将Page注入Model
        // 所以,在thymeleaf中可以直接访问Page中的数据,不需要再将Page添加到Model里面了
        page.setRows(discussPostService.findDiscussPostRows(0));
        page.setPath("/index");

        List<DiscussPost> list = discussPostService.findDiscussPosts(0, page.getOffset(), page.getLimit());
        List<Map<String, Object>> discussPosts = new ArrayList<>();
        if(list != null){
            for(DiscussPost post : list){
                Map<String, Object> map = new HashMap<>();
                map.put("post", post);
                User user = userService.findUserById(post.getUserId());
                map.put("user", user);
                discussPosts.add(map);
            }
        }
        model.addAttribute("discussPosts", discussPosts);
        return "/index";            // 返回templates目录下的index.html
    }

}

最后我们要修改index.html模板中的分页的内容

如果标签里面的内容是动态的话,一定要在标签的前面加上 th:

<!-- 分页 -->
<nav class="mt-5" th:if="${page.rows>0}">
   <ul class="pagination justify-content-center">
      <li class="page-item">

         <a class="page-link" th:href="@{${page.path}(current=1)}">首页</a>
      </li>
      <li th:class="|page-item ${page.current==1?'disabled':''}|">
         <a class="page-link" th:href="@{${page.path}(current=${page.current-1})}">上一页</a>
      </li>
      <li th:class="|page-item ${i==page.current?'active':''}|" th:each="i:${#numbers.sequence(page.from,page.to)}">
         <a class="page-link" th:href="@{${page.path}(current=${i})}" th:text="${i}">1</a>
      </li>
      <li th:class="|page-item ${page.current==page.total?'disabled':''}|">
         <a class="page-link" th:href="@{${page.path}(current=${page.current+1})}">下一页</a>
      </li>
      <li class="page-item">
         <a class="page-link" th:href="@{${page.path}(current=${page.total})}">末页</a>
      </li>
   </ul>
</nav>

image-20220705132207650.png

将测试,功能成功实现

下面是相关截图

image-20220705132250530.png

image-20220705132301999.png