1. 项目背景与意义
随着互联网信息量的爆炸式增长,新闻资讯的获取方式已经从传统的门户网站逐步转向个性化、社区化的内容平台。用户不再满足于被动接收编辑推荐的内容,而是希望参与讨论、发表观点、关注感兴趣的话题,并与志同道合的用户进行互动。传统的新闻网站往往以单向信息发布为主,缺乏用户参与和内容沉淀的机制,难以满足现代用户对信息获取和社交互动的双重需求。
新闻社区正是在这一背景下应运而生。它将新闻资讯的发布与用户社区互动相结合,既保留了新闻内容的权威性和时效性,又引入了评论、点赞、关注、收藏等社交化功能,形成了内容生产与用户消费的良性循环。通过社区化的运营,平台能够有效提升用户粘性、延长用户停留时间,并借助用户生成内容(UGC)降低内容生产成本,构建更具活力的内容生态。
从技术层面看,新闻社区系统涉及用户认证、内容管理、评论互动、搜索推荐、消息通知等多个核心模块,是一个典型的全栈 Web 应用场景。基于 Spring Boot 进行开发,能够充分利用其"约定优于配置"的设计理念、丰富的生态组件和成熟的社区支持,快速构建出结构清晰、易于维护、可扩展性强的企业级应用。因此,本课题的研究与实现不仅具有实际的应用价值,也对理解现代 Web 应用的分层架构、缓存策略、安全认证等核心技术具有重要的学习意义。
2. 系统需求分析
在系统设计之前,首先需要对新闻社区的核心业务需求进行梳理。本系统面向两类主要角色:普通用户和管理员。
2.1 用户端功能需求
- 用户注册与登录:支持邮箱或用户名注册,登录后维护个人会话状态。
- 新闻浏览:按分类、热度、时间等维度浏览新闻列表,支持关键词搜索。
- 新闻详情与评论:查看新闻全文,发表评论,回复他人评论,并对评论进行点赞。
- 互动功能:对新闻进行点赞、收藏,关注其他用户,查看个人关注动态。
- 个人中心:管理个人资料、查看我的发布、我的收藏、我的评论记录。
2.2 管理员端功能需求
- 新闻管理:发布、编辑、下架新闻内容,支持富文本编辑和封面图上传。
- 分类管理:维护新闻分类,调整分类排序与状态。
- 用户管理:查看用户列表,禁用违规账号,分配管理员角色。
- 评论审核:删除违规评论,维护社区氛围。
- 数据统计:查看新闻发布量、用户增长量、热门新闻排行等基础统计信息。
3. 技术栈选型
本系统采用前后端分离的开发模式,后端基于 Spring Boot 构建 RESTful API,前端使用 Vue.js 配合 Element UI 组件库实现页面交互。整体技术栈选型如下:
| 层次 | 技术选型 | 说明 |
|---|---|---|
| 后端框架 | Spring Boot 2.7.x | 快速构建独立运行的 Spring 应用,简化配置 |
| 持久层框架 | MyBatis-Plus | 增强 MyBatis,提供通用 CRUD、分页插件,减少样板代码 |
| 数据库 | MySQL 8.0 | 存储用户、新闻、评论、分类等核心业务数据 |
| 缓存 | Redis | 缓存热点新闻、验证码、用户会话,提升并发读取性能 |
| 安全认证 | Spring Security + JWT | 实现无状态认证与接口权限控制 |
| 搜索引擎 | Elasticsearch(可选) | 提供全文检索能力,优化新闻搜索体验 |
| 前端框架 | Vue.js 3 + Element Plus | 构建单页应用,组件化开发,提升开发效率 |
| 构建工具 | Maven / npm | 后端依赖管理与前端工程化构建 |
| 部署 | Docker + Nginx | 容器化部署,Nginx 反向代理与静态资源托管 |
在架构设计上,后端采用经典的分层架构:Controller 层负责接收请求与参数校验,Service 层承载业务逻辑,Mapper 层(DAO)负责数据库交互,Entity 层映射数据表结构。层与层之间通过接口解耦,便于单元测试与后续扩展。
4. 数据库设计
根据需求分析,系统核心数据表包括用户表、新闻表、分类表、评论表、点赞表、收藏表和关注表。下面给出主要表结构的简要设计。
4.1 用户表(user)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| username | varchar(50) | 用户名,唯一 |
| password | varchar(100) | 加密后的密码 |
| nickname | varchar(50) | 昵称 |
| avatar | varchar(255) | 头像 URL |
| role | tinyint | 角色:0 普通用户,1 管理员 |
| status | tinyint | 状态:0 正常,1 禁用 |
| create_time | datetime | 注册时间 |
4.2 新闻表(news)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| category_id | bigint | 所属分类 ID |
| title | varchar(200) | 新闻标题 |
| summary | varchar(500) | 新闻摘要 |
| content | text | 新闻正文(富文本 HTML) |
| cover_image | varchar(255) | 封面图 URL |
| author_id | bigint | 发布者 ID |
| view_count | int | 浏览量 |
| like_count | int | 点赞数 |
| status | tinyint | 状态:0 草稿,1 已发布,2 已下架 |
| create_time | datetime | 发布时间 |
4.3 评论表(comment)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| news_id | bigint | 所属新闻 ID |
| user_id | bigint | 评论用户 ID |
| parent_id | bigint | 父评论 ID,0 表示顶级评论 |
| content | varchar(1000) | 评论内容 |
| like_count | int | 点赞数 |
| create_time | datetime | 评论时间 |
此外,点赞表(news_like)、收藏表(news_favorite)和关注表(user_follow)均采用"用户 ID + 目标 ID"的联合唯一索引设计,用于记录用户与新闻、用户与用户之间的互动关系。分类表(category)结构较为简单,包含分类名称、排序号和状态字段。
5. 核心功能实现
本节选取系统中几个具有代表性的核心功能,给出关键代码实现与设计思路。
5.1 基于 JWT 的用户认证
系统采用 Spring Security 结合 JWT 实现无状态认证。用户登录成功后,后端签发一个包含用户 ID 和角色信息的 JWT Token,前端在后续请求中通过 Authorization 请求头携带该 Token。后端通过过滤器解析 Token 并设置安全上下文,从而识别当前登录用户。
首先定义一个 JWT 工具类,负责 Token 的生成与解析:
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.util.Date; import java.util.HashMap; import java.util.Map; @Component public class JwtUtil { @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private Long expiration; /** 生成 JWT Token */ public String generateToken(Long userId, String username, Integer role) { Map<String, Object> claims = new HashMap<>(); claims.put("userId", userId); claims.put("username", username); claims.put("role", role); Date now = new Date(); Date expireDate = new Date(now.getTime() + expiration * 1000); return Jwts.builder() .setClaims(claims) .setIssuedAt(now) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } /** 解析 Token,获取 Claims */ public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } /** 判断 Token 是否过期 */ public boolean isTokenExpired(String token) { return parseToken(token).getExpiration().before(new Date()); } }接下来编写 JWT 认证过滤器,在请求进入 Controller 之前完成 Token 校验与用户身份注入:
import io.jsonwebtoken.Claims; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Collections; @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtUtil jwtUtil; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = getTokenFromRequest(request); if (StringUtils.hasText(token) && SecurityContextHolder.getContext().getAuthentication() == null) { try { Claims claims = jwtUtil.parseToken(token); Long userId = claims.get("userId", Long.class); String username = claims.get("username", String.class); Integer role = claims.get("role", Integer.class); // 构造认证信息并放入安全上下文 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, Collections.emptyList()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); // 将 userId 和 role 存入 request 属性,便于业务层获取 request.setAttribute("userId", userId); request.setAttribute("role", role); } catch (Exception e) { // Token 无效或过期,不设置认证信息,由后续安全规则处理 logger.warn("JWT 解析失败: " + e.getMessage()); } } filterChain.doFilter(request, response); } private String getTokenFromRequest(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } }在 SecurityConfig 中配置放行规则,注册过滤器,并指定密码加密方式:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity public class SecurityConfig { @Autowired private JwtAuthenticationFilter jwtAuthenticationFilter; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/api/auth/", "/api/news/", "/api/category/").permitAll() .antMatchers("/api/admin/").hasRole("ADMIN") .anyRequest().authenticated() .and() .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } }5.2 新闻发布与分页查询
新闻发布是管理员的核心操作。Controller 接收前端提交的新闻表单数据,Service 层完成数据校验、封面图处理和数据库写入。这里使用 MyBatis-Plus 提供的 IService 简化 CRUD 操作。
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class NewsServiceImpl extends ServiceImpl<NewsMapper, News> implements NewsService { @Autowired private NewsMapper newsMapper; @Override @Transactional(rollbackFor = Exception.class) public boolean publishNews(News news, Long authorId) { news.setAuthorId(authorId); news.setStatus(1); // 1 表示已发布 news.setViewCount(0); news.setLikeCount(0); return save(news); } @Override public PageResult<NewsVO> getNewsPage(int page, int size, Long categoryId, String keyword) { Page<News> pageParam = new Page<>(page, size); LambdaQueryWrapper<News> wrapper = new LambdaQueryWrapper<>(); if (categoryId != null) { wrapper.eq(News::getCategoryId, categoryId); } if (StringUtils.hasText(keyword)) { wrapper.like(News::getTitle, keyword).or().like(News::getSummary, keyword); } wrapper.eq(News::getStatus, 1).orderByDesc(News::getCreateTime); Page<News> result = newsMapper.selectPage(pageParam, wrapper); // 转换为 VO,补充分类名称和作者昵称等展示字段 List<NewsVO> voList = result.getRecords().stream().map(news -> { NewsVO vo = new NewsVO(); BeanUtils.copyProperties(news, vo); return vo; }).collect(Collectors.toList()); return new PageResult<>(voList, result.getTotal()); } }对应的 Controller 层代码如下:
<