从今天开始我们来写一个新项目,这个项目是一个完整的校园论坛的项目。主要功能模块:用户登录注册,帖子发布和热帖排行,点赞关注,发送私信,消息通知,社区搜索等。这篇文章我们先试着写一下用户的登录注册功能。
我们做web项目,一般web项目是主要解决浏览器和服务器之间交互的问题。而浏览器和服务器是由一次一次的请求交互的。因此,任何功能都可拆解成若干次请求,其实只要掌握好每次请求的执行过程,按照步骤开发每一次请求,基本上项目就可以逐步完善起来。
一次请求的执行过程:

其实最好是可以把功能做拆解,第一步先实现什么效果,第二步再完善这个效果。因为你不可能一下子就一步到位写出完整的代码的,是吧?
所以这就是我的思路,我是想着一个功能一个功能的搞定。这篇文章来搞定开发社区首页。

首页有什么?不就是帖子,还有用户头像,还有分页嘛,因此接下来我们就实现这三个功能。
entity文件夹:Page.java
DAO层:DiscussionMapper
Service层:DiscussionPostMapper
Controller层:HomeController
DiscussPostMapper- @Mapper
- public interface DiscussPostMapper {
-
- List
selectDiscussPosts(int userId, int offset, int limit, int orderMode); -
- // @Param注解用于给参数取别名,
- // 如果只有一个参数,并且在
里使用,则必须加别名. - 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更新帖子的分数信息。
DiscussPostService-
-
- @Service
- public class DiscussPostService {
-
- @PostConstruct
- public void init() {
- // 初始化帖子列表缓存
- postListCache = Caffeine.newBuilder()
- .maximumSize(maxSize)
- .expireAfterWrite(expireSeconds, TimeUnit.SECONDS)
- .build(new CacheLoader
>() { - @Nullable
- @Override
- public List
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
() { - @Nullable
- @Override
- public Integer load(@NonNull Integer key) throws Exception {
- logger.debug("load post rows from DB.");
- return discussPostMapper.selectDiscussPostRows(key);
- }
- });
- }
-
- public List
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的服务组件。
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
list = discussPostService - .findDiscussPosts(0, page.getOffset(), page.getLimit(), orderMode);
- List
- if (list != null) {
- for (DiscussPost post : list) {
- Map
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中供前端页面渲染。
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;
- }
-
- }
这个实体类用于在分页查询中保存分页相关的信息,例如当前页码、每页显示的数量、数据总数等,以便在前端页面进行分页展示和生成分页链接。
至此,社区首页的一些简单的功能就实现了。其实这并不复杂,就是代码有点多而已。