开发仿牛客网论坛首页。
① 表user
包含字段:username
, password
, salt
, email
, type
, status
, activation_code
, header_url
, create_time
② 表discuss_post
包含字段:
id
, user_id
, title
, content
, type
, status
, create_time
, comment_count
, score
实体类:DiscussPost.java
package com.nowcoder.community.entity;
import java.util.Date;
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;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public int getCommentCount() {
return commentCount;
}
public void setCommentCount(int commentCount) {
this.commentCount = commentCount;
}
public double getScore() {
return score;
}
public void setScore(double score) {
this.score = score;
}
@Override
public String toString() {
return "DiscussPost{" +
"id=" + id +
", userId=" + userId +
", title='" + title + '\'' +
", content='" + content + '\'' +
", type=" + type +
", status=" + status +
", createTime=" + createTime +
", commentCount=" + commentCount +
", score=" + score +
'}';
}
}
实体类:Page.java
该类用于封装分页相关的信息。
package com.nowcoder.community.entity;
/**
* 封装分页相关的信息.
*/
public class Page {
// 当前页码
private int current = 1;
// 显示上限
private int limit = 10;
// 数据总数(用于计算总页数)
private int rows;
// 查询路径(用于复用分页链接)
private String path;
public int getCurrent() {
return current;
}
// 当前页页码必须不小于1
public void setCurrent(int current) {
if (current >= 1) {
this.current = current;
}
}
public int getLimit() {
return limit;
}
// 显示数据的下限为1,上限为100
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;
}
}
接口:DiscussPostMapper.java
package com.nowcoder.community.dao;
import com.nowcoder.community.entity.DiscussPost;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface DiscussPostMapper {
List<DiscussPost> selectDiscussPosts(int userId, int offset, int limit);
// @Param注解用于给参数取别名
// 如果只有一个参数,并且在里使用,则必须加别名
int selectDiscussPostRows(@Param("userId") int userId);
}
@Param
注解用于给参数取别名,如果只有一个参数,并且在里使用,则必须加别名。
discusspost-mapper.xml
实现SQL
与实体类DiscussPost
的映射。
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="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="int">
select count(id)
from discuss_post
where status != 2
<if test="userId != 0">
and user_id = #{userId}
if>
select>
mapper>
其中,第一个 select 语句相当于:
select id, user_id, title, content, type, status, create_time, comment_count, score
from discuss_post
where status != 2
order by type desc, create_time desc
limit ?, ?
第二个 select 语句相当于:
select count(id)
from discuss_post
where status != 2
业务组件:DiscussPortService.java
package com.nowcoder.community.service;
import com.nowcoder.community.dao.DiscussPostMapper;
import com.nowcoder.community.entity.DiscussPost;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class DiscussPostService {
@Autowired
private DiscussPostMapper discussPostMapper;
public List<DiscussPost> findDiscussPosts(int userId, int offset, int limit) {
return discussPostMapper.selectDiscussPosts(userId, offset, limit);
}
public int findDiscussPostRows(int userId) {
return discussPostMapper.selectDiscussPostRows(userId);
}
}
控制组件:HomeController.java
注入Service组件:DiscussPostService
、UserService
方法getIndexPage()
中的参数 model 和 page均由前端控制器DispatcherServlet 自动创建。
package com.nowcoder.community.controller;
import com.nowcoder.community.entity.DiscussPost;
import com.nowcoder.community.entity.Page;
import com.nowcoder.community.entity.User;
import com.nowcoder.community.service.DiscussPostService;
import com.nowcoder.community.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Controller
public class HomeController {
@Autowired
private DiscussPostService discussPostService;
@Autowired
private UserService userService;
@GetMapping("/index")
public String getIndexPage(Model model, Page page) {
// 方法调用前,SpringMVC会自动实例化Model和Page,并将Page注入Model
// 所以,在thymeleaf中可以直接访问Page对象中的数据
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";
}
}
① list 是封装了帖子 DiscussPost (包含多个字段的实体类)的List
② discussPosts 是封装了 map 的 List
③ map 中封装了 帖子 post 和对应的 用户实体类对象 user(通过 post 根据user_id 查询得到)
论坛首页index.html
① 帖子列表部分:
<!-- 帖子列表 -->
<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:utext="${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:each="map:${discussPosts}"
使用变量 map 遍历封装了帖子和对应用户实体类对象的集合构成的列表 discussPosts。th:src="${map.user.headerUrl}"
用于动态替换 map 中的 user 的用户头像存储链接。th:utext="${map.post.title}"
用于动态替换 map 中的 post 的帖子标题。th:if="${map.post.type==1}"
用于条件判断,动态替换帖子类型为 1,即置顶的帖子。th:if="${map.post.status==1}"
用于条件判断,动态替换帖子状态为 1,即精华帖。th:utext="${map.user.username}"
用于动态替换 map 中的用户的用户名。th:text="${#dates.format(map.post.createTime,'yyyy-MM-dd HH:mm:ss')}"
用于按照指定格式动态生成日期。② 分页部分:
<!-- 分页 -->
<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>
th:if="${page.rows>0}
用于条件判断该页数据条数是否为正数。th:href="@{${page.path}(current=1)}"
首页th:class="|page-item ${page.current==1?'disabled':''}|"
th:href="@{${page.path}(current=${page.current-1})}"
上一页th:class="|page-item ${i==page.current?'active':''}|"
th:each="i:${#numbers.sequence(page.from,page.to)}"
th:href="@{${page.path}(current=${i})}"
th:text="${i}"
页码1th:class="|page-item ${page.current==page.total?'disabled':''}|"
th:href="@{${page.path}(current=${page.current+1})}"
下一页th:href="@{${page.path}(current=${page.total})}"
末页