从今天开始我们来写一个新项目,这个项目是一个完整的校园论坛的项目。主要功能模块:用户登录注册,帖子发布和热帖排行,点赞关注,发送私信,消息通知,社区搜索等。这篇文章我们先试着写一下用户的登录注册功能。
我们做web项目,一般web项目是主要解决浏览器和服务器之间交互的问题。而浏览器和服务器是由一次一次的请求交互的。因此,任何功能都可拆解成若干次请求,其实只要掌握好每次请求的执行过程,按照步骤开发每一次请求,基本上项目就可以逐步完善起来。
一次请求的执行过程:
其实最好是可以把功能做拆解,第一步先实现什么效果,第二步再完善这个效果。因为你不可能一下子就一步到位写出完整的代码的,是吧?
所以这就是我的思路,我是想着一个功能一个功能的搞定。这篇文章来搞定开发社区首页。
首页有什么?不就是帖子,还有用户头像,还有分页嘛,因此接下来我们就实现这三个功能。
社区首页模块大致结构
entity文件夹:Page.java
DAO层:DiscussionMapper
Service层:DiscussionPostMapper
Controller层:HomeController
Dao层---DiscussPostMapper
@Mapper
public interface DiscussPostMapper {
List<DiscussPost> selectDiscussPosts(int userId, int offset, int limit, int orderMode);
// @Param注解用于给参数取别名,
// 如果只有一个参数,并且在<if>里使用,则必须加别名.
int selectDiscussPostRows(@Param("userId") int userId);
int insertDiscussPost(DiscussPost discussPost);
DiscussPost selectDiscussPostById(int id);
int updateCommentCount(int id, int commentCount);
int updateType(int id, int type);
int updateStatus(int id, int status);
int updateScore(int id, double score);
}
这是一个名为DiscussPostMapper
的接口,用于定义帖子相关的数据库操作方法 :
-
selectDiscussPosts(int userId, int offset, int limit, int orderMode)
:根据用户ID、偏移量、限制数量和排序方式从数据库中查询帖子列表。 -
selectDiscussPostRows(int userId)
:查询帖子总数。根据用户ID统计数据库中的帖子数量。 -
insertDiscussPost(DiscussPost discussPost)
:将帖子信息插入数据库。 -
selectDiscussPostById(int id)
:根据帖子ID从数据库中查询帖子信息。 -
updateCommentCount(int id, int commentCount)
:更新帖子的评论数量。根据帖子ID更新帖子的评论数量信息。 -
updateType(int id, int type)
:更新帖子的类型。根据帖子ID更新帖子的类型信息。 -
updateStatus(int id, int status)
:更新帖子的状态。根据帖子ID更新帖子的状态信息。 -
updateScore(int id, double score)
:更新帖子的分数。根据帖子ID更新帖子的分数信息。
Service层---DiscussPostService
@Service
public class DiscussPostService {
@PostConstruct
public void init() {
// 初始化帖子列表缓存
postListCache = Caffeine.newBuilder()
.maximumSize(maxSize)
.expireAfterWrite(expireSeconds, TimeUnit.SECONDS)
.build(new CacheLoader<String, List<DiscussPost>>() {
@Nullable
@Override
public List<DiscussPost> load(@NonNull String key) throws Exception {
if (key == null || key.length() == 0) {
throw new IllegalArgumentException("参数错误!");
}
String[] params = key.split(":");
if (params == null || params.length != 2) {
throw new IllegalArgumentException("参数错误!");
}
int offset = Integer.valueOf(params[0]);
int limit = Integer.valueOf(params[1]);
// 二级缓存: Redis -> mysql
logger.debug("load post list from DB.");
return discussPostMapper.selectDiscussPosts(0, offset, limit, 1);
}
});
// 初始化帖子总数缓存
postRowsCache = Caffeine.newBuilder()
.maximumSize(maxSize)
.expireAfterWrite(expireSeconds, TimeUnit.SECONDS)
.build(new CacheLoader<Integer, Integer>() {
@Nullable
@Override
public Integer load(@NonNull Integer key) throws Exception {
logger.debug("load post rows from DB.");
return discussPostMapper.selectDiscussPostRows(key);
}
});
}
public List<DiscussPost> findDiscussPosts(int userId, int offset, int limit, int orderMode) {
if (userId == 0 && orderMode == 1) {
return postListCache.get(offset + ":" + limit);
}
logger.debug("load post list from DB.");
return discussPostMapper.selectDiscussPosts(userId, offset, limit, orderMode);
}
public int findDiscussPostRows(int userId) {
if (userId == 0) {
return postRowsCache.get(userId);
}
logger.debug("load post rows from DB.");
return discussPostMapper.selectDiscussPostRows(userId);
}
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);
}
public DiscussPost findDiscussPostById(int id) {
return discussPostMapper.selectDiscussPostById(id);
}
public int updateCommentCount(int id, int commentCount) {
return discussPostMapper.updateCommentCount(id, commentCount);
}
public int updateType(int id, int type) {
return discussPostMapper.updateType(id, type);
}
public int updateStatus(int id, int status) {
return discussPostMapper.updateStatus(id, status);
}
public int updateScore(int id, double score) {
return discussPostMapper.updateScore(id, score);
}
}
这是一个名为DiscussPostService
的服务类,用于处理帖子相关的业务逻辑。这段代码是真的烦,一大堆东西,不过我总结了一下,大概这段代码的功能差不多是这样的:
-
通过
DiscussPostMapper
进行数据库操作,包括查询帖子列表、帖子总数,插入帖子,根据帖子ID查询帖子信息,以及更新帖子的评论数量、类型、状态和分数。 -
使用
SensitiveFilter
进行敏感词过滤,对帖子的标题和内容进行转义和过滤。 -
使用Caffeine缓存库对帖子列表和帖子总数进行缓存,提高访问性能。缓存设置了最大容量和过期时间。
-
在初始化方法
init()
中,配置了帖子列表缓存和帖子总数缓存的加载方式,当缓存中不存在数据时,会从数据库中加载数据。 -
提供了方法如
findDiscussPosts()
和findDiscussPostRows()
来获取帖子列表和帖子总数,如果缓存中有数据,则直接从缓存中获取,否则从数据库中获取。 -
提供了方法如
addDiscussPost()
来添加帖子,对帖子的标题和内容进行处理后插入数据库。 -
该服务类使用了注解
@Service
,表示该类是一个Spring的服务组件。
Controller层---HomeController
@Controller
public class HomeController implements CommunityConstant {
@RequestMapping(path = "/", method = RequestMethod.GET)
public String root() {
return "forward:/index";
}
@RequestMapping(path = "/index", method = RequestMethod.GET)
public String getIndexPage(Model model, Page page,
@RequestParam(name = "orderMode", defaultValue = "0") int orderMode) {
// 方法调用前,SpringMVC会自动实例化Model和Page,并将Page注入Model.
// 所以,在thymeleaf中可以直接访问Page对象中的数据.
page.setRows(discussPostService.findDiscussPostRows(0));
page.setPath("/index?orderMode=" + orderMode);
List<DiscussPost> list = discussPostService
.findDiscussPosts(0, page.getOffset(), page.getLimit(), orderMode);
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);
long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, post.getId());
map.put("likeCount", likeCount);
discussPosts.add(map);
}
}
model.addAttribute("discussPosts", discussPosts);
model.addAttribute("orderMode", orderMode);
return "/index";
}
@RequestMapping(path = "/error", method = RequestMethod.GET)
public String getErrorPage() {
return "/error/500";
}
@RequestMapping(path = "/denied", method = RequestMethod.GET)
public String getDeniedPage() {
return "/error/404";
}
}
这是一个名为HomeController
的控制器类,用于处理主页相关的请求。
-
通过
DiscussPostService
获取帖子相关的信息,包括帖子列表和帖子总数。 -
通过
UserService
获取用户相关的信息,包括发帖用户的信息。 -
通过
LikeService
获取帖子的点赞数量。 -
提供了
root()
方法,将根路径的请求转发到"/index"路径。 -
提供了
getIndexPage()
方法,处理主页的GET请求,根据页面参数orderMode
获取帖子列表,并将数据添加到Model
中供前端页面渲染。
entity文件夹---Page
/**
* 封装分页相关的信息.
*/
public class Page {
// 当前页码
private int current = 1;
// 显示上限
private int limit = 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;
}
}
这个实体类用于在分页查询中保存分页相关的信息,例如当前页码、每页显示的数量、数据总数等,以便在前端页面进行分页展示和生成分页链接。
至此,社区首页的一些简单的功能就实现了。其实这并不复杂,就是代码有点多而已。