做新闻管理系统这个项目,最早是因为帮一家公司做内部信息平台,后来又把同一套思路整理成了一个可以开源、可以直接跑起来的完整全栈项目,技术栈就是Java+Springboot+Vue。说实话,这类项目在网上能找到的源码非常多,但大部分要么缺数据库脚本,要么少前端配置,要么文档写得含糊其辞,新手拿到手根本跑不起来。这篇文章就围绕这个新闻管理系统的源码,把项目结构、表设计、核心接口、Vue前端实现、完整的运行步骤,以及我在实际开发中踩过的一堆坑,一次性讲透。
如果你正需要一份能跑通、能写进简历、能应付课程设计或毕业设计的全栈项目,这篇内容应该能帮你省掉不少自己摸索的时间。代码部分我会给出关键示例,运行部分给到具体命令,坑的部分则写完整的排查链路,而不是直接丢个"改这个配置就好"的结论。
1. 做新闻管理系统之前,先想清楚这几件事
1.1 它要解决的业务问题到底是什么
很多人把新闻管理系统理解成一个"发布文章的后台",这个理解太浅了。真正在业务里跑起来的新闻管理系统,核心其实是内容生命周期管理:一篇文章从编辑起草,到提交审核,到正式发布,再到因为违规或过期被下架,每一步都有对应的状态和权限控制。总结下来,它要解决的问题主要有这么几类:
- 内容源头的管理:编辑需要能创作、保存草稿、修改自己的文章,但不能直接发布。
- 审核与发布流程:管理员或审核员查看待审核内容,选择通过或驳回,通过后自动设置发布时间。
- 内容展示与检索:门户端按分类展示新闻、支持按关键词搜索、记录浏览量。
- 多角色权限:普通用户只能浏览和评论,编辑管理自己的内容,管理员拥有全部权限。
这也是我推荐用这个项目练手的原因:它表面是增删改查,实际上包含了状态机、权限模型、文件上传、富文本处理、前后端分离联调这些非常常见的真实场景。做完这个项目,你对一个完整业务系统的认知会清晰很多。
我做的第一版其实就是个简单的CRUD,用户表、新闻表、分类表,后台能发文章前台能看。但拿到真实场景里发现根本不够用——编辑误操作把没写完的文章发出去了,审核环节缺失,出了问题没人能追责。后来我重新设计状态字段,把发布流程纳入系统,才算是真正"能上线"的新闻管理系统。
1.2 Java + Springboot + Vue 这个组合为什么是黄金搭档
技术选型这块,我得说实话:这个组合并不是所有场景里性能最好的,但它是目前国内做这类业务系统最成熟、招人需求最大的组合之一。
Java这个语言本身就不用多说了,强类型、生态庞大、企业级应用的主战场。Springboot解决了传统Spring项目配置繁琐的问题,内嵌Tomcat,一条命令就能启动,非常适合快速交付。Vue作为前端框架,中文社区活跃、上手曲线平缓、组件化开发效率高,配合Element UI这类组件库,后台管理界面做起来非常顺手。
这个组合还有一个实打实的优势:面试和就业价值。Java面试题里Springboot占了很大比重,Vue也是前端岗位的常考内容。一个既写了Springboot后端又写了Vue前端的完整项目,在简历上的说服力远比"跟着教程敲过某个登录页面"强得多。我见过不少同事靠这类项目拿到第一份开发岗Offer,真不是玄学。
下面这张表是我个人对这个技术栈的评估,供你在选型时参考:
| 维度 | 评估 | 说明 |
|---|---|---|
| 开发效率 | 高 | Springboot自动配置 + Vue组件化,单人完成全栈是可行的 |
| 性能与扩展 | 中上 | 单体部署足够支撑中小型业务,后续可平滑扩展 |
| 学习资源 | 极丰富 | 中文资料多、社区活跃,遇到问题基本都能搜到 |
| 面试匹配度 | 很高 | 大厂小厂都在招这个栈的工程师 |
| 运维成本 | 低 | 后端一个Jar包、前端静态文件,甚至能合并部署 |
2. 数据库设计与后端核心模块拆解
2.1 四张核心表的设计:用户、分类、新闻、评论
数据库设计是这个项目最值得花时间的部分,很多细节在写代码时才会暴露出来。我最终的表结构里,最核心的是四张表:sys_user(用户)、category(分类)、news(新闻)、comment(评论)。
先看用户表。密码不能存明文,我用的是BCrypt加密后的字符串;role字段用字符串而不是数字,可读性更好;status字段控制账号是否被禁用。
CREATE TABLE `sys_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码(BCrypt加密)', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `avatar` varchar(255) DEFAULT NULL COMMENT '头像URL', `role` varchar(20) NOT NULL DEFAULT 'user' COMMENT '角色: admin/editor/user', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态: 1正常 0禁用', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';新闻表是核心。这里最关键的字段是status,它承载了整个发布流程的状态流转。我定义了四个状态:0草稿、1待审核、2已发布、3已下架。status配合publish_time,就可以实现"审核通过后定时发布"这类操作。
CREATE TABLE `news` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `title` varchar(200) NOT NULL COMMENT '新闻标题', `summary` varchar(500) DEFAULT NULL COMMENT '摘要', `content` longtext COMMENT '正文HTML内容', `cover_image` varchar(255) DEFAULT NULL COMMENT '封面图URL', `category_id` bigint(20) DEFAULT NULL COMMENT '分类ID', `author_id` bigint(20) NOT NULL COMMENT '作者ID', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态: 0草稿 1待审核 2已发布 3已下架', `view_count` int(11) NOT NULL DEFAULT '0' COMMENT '浏览量', `publish_time` datetime DEFAULT NULL COMMENT '发布时间', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='新闻表';分类表和评论表相对简单,但有几个点要注意。分类表的sort字段用于控制前台展示顺序;评论表里除了news_id和user_id两个外键,也保留了一个status字段,目的是支持后台删除评论时用逻辑删除而不是物理删除,避免把用户的评论痕迹彻底抹掉,这个在真实运营场景里是个合规细节。
2.2 Springboot三层架构:Controller-Service-Mapper怎么写
后端我采用的是标准的Controller-Service-Mapper三层结构,持久层框架选的是MyBatis-Plus,而不是JPA。原因很直接:MyBatis-Plus在国内团队用得多,SQL可控性强,分页插件、逻辑删除、代码生成器这些功能都很成熟,遇到复杂查询时写SQL也不会被框架限制住。
Controller层只负责接收参数和返回统一结构。我封装了一个Result类,所有接口统一返回code、message、data三个字段,前端判断code是否为200来决定逻辑走向。这样写的好处是整个项目的接口风格一致,前端联调时几乎不用猜每个接口返回什么结构。
@RestController @RequestMapping("/api/news") public class NewsController { @Autowired private NewsService newsService; @GetMapping("/page") public Result<PageResult<NewsVO>> page( @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) Long categoryId, @RequestParam(required = false) String keyword) { return Result.success(newsService.pageNews(pageNum, pageSize, categoryId, keyword)); } @PostMapping public Result<Void> create(@RequestBody @Valid NewsDTO dto) { newsService.createNews(dto); return Result.success(); } }Service层是业务逻辑的核心,也是整个项目里最需要动脑子的地方。以"创建新闻"为例,它的完整逻辑链路是:
- 从当前登录用户上下文里取出用户id,作为author_id。
- DTO字段校验,包括标题不能为空、分类必须存在、内容长度限制。
- 根据用户角色决定初始状态:管理员直接置为"已发布",编辑置为"待审核"。
- 如果状态是发布,则同时写入publish_time。
这种状态判断不要散落在Controller里,一定要收敛到Service层,否则后续加一个"定时发布"功能时你会改得很痛苦。我第一版就是状态逻辑写在哪里都有一点,结果需求一变,到处都要动,那次之后我彻底改成在Service里定义状态变更的私有方法,外部只允许调用。
2.3 JWT登录鉴权:权限控制怎么落地
登录鉴权这个模块,是这个项目里含金量比较高的部分,也是很多教程项目最敷衍的部分。我采用的是JWT + 拦截器的方案,不使用Session,因为前后端分离后Session有跨域和集群同步的问题,JWT天然无状态,后端不用存登录态。
JWT的本质是一个包含三段内容的字符串:Header(算法声明)、Payload(用户信息)、Signature(签名)。后端签发的时候用密钥签名,前端每次请求带上这个Token,后端拦截器验证签名合法后,把用户信息放进请求上下文。
public class JwtUtil { private static final String SECRET = "your-secret-key-change-in-production"; private static final long EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000L; public static String generateToken(Long userId, String role) { return Jwts.builder() .setSubject(userId.toString()) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } }拦截器这一层要做的就是三件事:从请求头取Token、验签、把userId和role塞到ThreadLocal里供后续Service使用。注意一个细节:白名单接口要显式放行,比如登录接口、前台新闻列表和详情,这些接口不需要登录也能访问,否则一上线就全站404加401报警。
权限控制的核心是角色判断要集中在注解或拦截器里,而不是散落在每个Service方法内。我用了一个简单的自定义注解@RequireRole("admin"),放在需要管理员权限的Controller方法上,拦截器里统一做校验。这种做法的好处是你扫一眼Controller就知道哪个接口谁能访问,审查权限的时候非常高效。
3. Vue前端的关键实现:从后台管理到门户展示
3.1 前端工程化结构与路由设计
前端我分了两个部分:面向普通访客的门户页面和面向编辑、管理员的后台管理页面。两者用同一个Vue工程承载,但路由模块完全隔离,这样复用的是组件和工具函数,业务空间互不干扰。
工程目录我建议这样组织,比大一统的views目录清晰很多:
src/ api/ # axios请求模块,按业务拆文件 assets/ # 静态资源 components/ # 公共组件 router/ # 路由配置 index.js modules/ portal.js # 前台路由 admin.js # 后台路由 store/ # Pinia状态管理 views/ portal/ # 前台页面 admin/ # 后台页面 utils/ # 工具函数路由设计上,我使用了路由守卫来做登录拦截。在后台上路由meta里标记requiresAuth和role,前端在beforeEach里判断当前用户是否已登录、是否具备访问权限。这个方案比后端返回401后再跳转体验好得多,用户在点击进入后台的第一时间就能被引导到登录页。
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next('/login') } else if (to.meta.role === 'admin' && getRole() !== 'admin') { next('/403') } else { next() } })很多新手在这里会犯一个错误:只看前端路由守卫,以为页面没暴露就安全了。实际上后端接口权限才是根本,前端路由守卫只是用户体验优化。我见过有人把后端接口完全开放,只在前端藏了管理入口,结果被人直接调API改数据,教训极其深刻。
3.2 富文本编辑、图片上传与XSS过滤
新闻正文不能用一个普通文本框,需要富文本编辑器。我这里选的是wangEditor,纯粹因为它对中文场景支持好、上手简单,引入后配置一个toolbar和editor区域就能用。但富文本编辑器带来的核心问题不是"怎么选",而是"安全性"——用户可以在正文里塞任意HTML,包括script标签。
前端在提交内容前过滤一遍,后端在接收内容时必须再过滤一遍。后端过滤我推荐使用jsoup这个库,它可以把HTML里的危险标签和事件属性清洗掉,保留安全的格式标签。千万不要只做前端过滤,因为绕过前端直接POST接口太容易了。
图片上传这块,我的做法是单独提供一个/api/upload接口,前端先把图片上传到服务器拿到URL,再把这个URL拼进富文本内容。这个方案比base64直接嵌入正文要合理得多,否则一篇带十张图的新闻光正文就有几兆,数据库和接口都会很吃力。
3.3 前后端联调最容易出问题的几个点
前后端联调是新手最容易卡住的地方,我列几个我踩过且几乎每个项目都会遇到的重灾区。
第一个是跨域。前端跑在3000端口,后端跑在8080端口,浏览器直接请求就会被跨域拦截。解决办法我放在后面第5节详细讲,这里先记住:开发环境用前端代理,生产环境用Nginx或者后端合并部署,这是最稳的两条路。
第二个是日期格式。Java后端默认序列化的日期格式是ISO格式带毫秒,Vue这边拿到数据后显示会很奇怪。统一处理方案是在application.yml里配置全局的日期序列化格式,让后端输出的时间统一是yyyy-MM-dd HH:mm:ss,前端不用再到处做转换。
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8第三个是字段命名习惯。Java习惯驼峰命名(publishTime),MySQL里我用的是下划线(publish_time),如果使用MyBatis-Plus的自动映射,默认会把下划线转驼峰,但要确认配置没被关掉。前端定义VO时也尽量跟后端约定好的字段名一致,省得联调时在对象属性名上扯皮。
4. 源码拿到手怎么跑起来:完整运行步骤
4.1 环境准备:JDK、Maven、Node.js、MySQL版本怎么配
拿到源码之后第一步不是急着双击运行,而是把环境对齐。很多项目跑不起来,根子出在版本不一致上。我这里给出我开发时使用的版本组合,你照着装基本不会再出兼容性问题。
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 或 11 | 不要用17+,除非你确定依赖全部兼容 |
| Maven | 3.6.x | 3.8以上也能用,但换源配置稍有差别 |
| Node.js | 14.x 或 16.x | 高版本npm安装依赖时可能报旧模块错误 |
| MySQL | 5.7 或 8.0 | 8.0注意驱动要配mysql-connector-java 8.x |
| IDE | IDEA 2020+ | 社区版够用,专业版更好 |
这里我要单独提醒一点:前两年开始Spring官方把Springboot版本推得很快,3.x版本要求JDK 17,且很多老依赖不兼容。如果你是一个新手,我强烈建议先用Springboot 2.7.x + JDK 8这个组合把项目跑起来,这会省去大量因为版本导致的折腾。后面有时间再研究升级迁移,而不是一上来就挑战最高版本。
4.2 后端启动:配置文件修改到运行
环境装好之后,先改配置文件。找到src/main/resources/application.yml,里面最核心的就是数据库连接:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/news_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your-password driver-class-name: com.mysql.cj.jdbc.Driver注意三件事:第一,数据库名news_system要先建好,编码选utf8mb4;第二,serverTimezone一定要配,否则时间相关字段会有时区偏差;第三,密码别用弱口令,这是项目上线的大忌。
然后执行后端启动命令。在项目根目录下(有pom.xml的那一层)打开终端:
mvn clean package -DskipTests java -jar target/news-system.jar如果你的机器装了Maven但没有配好阿里云镜像,第一次打包拉依赖会非常慢。建议在Maven的settings.xml里配置mirror为阿里云中央仓库,这个步骤能省下大把时间。看到类似Tomcat started on port(s): 8080的日志,后端就算起来了。后端单独启动时,可以先在浏览器访问一下Swagger层面的路径(如果项目里集成了knife4j或Springdoc,通常是/doc.html),确认接口层是否正常。
4.3 前端启动:依赖安装与代理配置
后端起来后,打开前端的命令行窗口,cd到前端目录(通常是news-frontend)。
npm install npm run servenpm install这一步是最容易出状况的,常见的错误有node-sass版本与Node版本不匹配、依赖包下载超时等。我的建议是直接使用国内镜像源:
npm config set registry https://registry.npmmirror.com前端开发服务器的跨域代理配置在vue.config.js里。核心逻辑是把前端对/api的请求转发到后端的8080端口:
module.exports = { devServer: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }配置完成后,浏览器访问http://localhost:3000,前端页面里的接口请求会通过代理自动转发到后端,不需要在代码里写死后端地址。这里有个我实际开发中踩过的坑:如果后端接口路径不是以/api开头,代理就匹配不上,联调时会看到前端有请求但全是404,排查时第一件事就是确认路径前缀是否一致。
4.4 初始化数据与默认账号
项目里通常带一个news_system.sql脚本,需要在MySQL里执行一次,把表结构和基础数据初始化进去。执行完检查一下sys_user表,里面应该有一个初始的管理员账号,我这边默认是admin,密码123456,后续启动后第一件事建议改成自己的密码。
顺便说一下登录验证的完整流程,方便你确认是否跑通了:打开门户首页,能看到初始化脚本里插入的测试新闻;进入后台登录页,用admin账号登录,系统签发JWT存入本地存储;如果编辑一篇新闻提交审核,状态会从未审核变为待审核;再用审核账号通过,前台刷新即可看到新发布的内容。
5. 实际开发中踩过的坑与排查思路
5.1 跨域问题的完整排查链路
这个坑几乎每个做前后端分离项目的人都会遇到,值得把排查链路完整写一遍。现象是:前端页面正常打开,但network面板里所有请求标红,console报错。浏览器明确提示"Access-Control-Allow-Origin"之类的内容,中文翻译过来大致是"跨域请求被阻止"。
我第一次遇到时,第一反应是去后端加@CrossOrigin注解,加完之后有的接口好了有的没好,人更懵了。后来静下来理了一遍才明白,跨域问题的本质是浏览器基于同源策略做的拦截,请求其实发出去了,后端也响应了,但浏览器不让前端读取响应。所以排查的第一步不是加配置,而是确认是不是真的跨域:对比前端页面地址(协议+域名+端口)和后端接口地址,只要有一个不同,必然触发跨域。
解决方案按场景区分。开发期最优雅的方式是我前面说的前端代理,把/api请求转发到后端,浏览器看到的是同源请求,不存在跨域。生产期如果前端和后端部署在不同域名下,后端统一配置CorsFilter更合适。
@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }另一个常被忽略的点是:如果使用了Spring Security或自定义拦截器,CORS配置必须在拦截器链的最外层生效,否则请求会被鉴权拦截器先一步拦截返回401,浏览器自然拿不到预期的跨域头,表现同样是请求失败。
5.2 Springboot版本过高引发的依赖冲突
这个问题这两年特别多,因为我前面提过,Spring官方持续在推进版本升级,很多教程和代码还在用旧版写法。我看到网上有人问"springboot版本太高怎么办",其实核心不是版本高低问题,而是整套依赖的兼容矩阵没有对齐。
举个例子,我之前给一个项目引入MyBatis-Plus时,用的版本还是3.4.x,Springboot用的3.0.x,结果启动直接报错,原因是MyBatis-Plus老版本里的某些类依赖了Spring Boot 2.x中才有的自动配置类。排查到最后,要么把MyBatis-Plus升级到适配Springboot 3.x的新版,要么把Springboot降回2.7。
这个教训给我的启发是:遇到依赖冲突时,不要只盯着一个包的版本,要看整个依赖树。命令行的dependency:tree可以帮助看清当前项目实际依赖了哪些版本,排查冲突时非常有效。对新手来说,最稳妥的做法就是直接使用Spring Initializr生成的默认版本组合,再逐步引入其他依赖,不要手动拼接版本号。
5.3 Vue打包后部署到Springboot的静态资源坑
本地用npm run serve跑得好好的,但执行npm run build之后把dist目录放到服务器上,访问首页是404。这个问题我帮人排查过很多次,原因通常是前端路由使用了history模式。history模式下的路由是浏览器history API模拟的,访问一个不存在的路径时,服务器找不到对应资源,返回404。
解决思路是让服务器把所有请求都重定向到index.html,由前端路由接管。有三种落地方式:Nginx配置try_files、Springboot编写一个controller转发,或者是修改前端路由为hash模式。我推荐生产环境直接上Nginx,把打包好的dist目录作为静态资源目录,配置如下:
location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; }还有一种常被问到的部署方式是"把前端打包产物直接塞进Springboot的resources/static目录",做成一个Jar前后端一把梭。这种方案的优点是部署简单,缺点是前端更新一次就得重新打一次后端包,团队协作时不方便。所以在我的项目里,这两种方案都能跑通,但我本人更推荐前后端分开部署,扩展空间更大。
从时间线上看,这类问题的排查思路基本是"先确认请求到了哪一层",我一般用浏览器F12看网络请求的响应状态,如果是Pure HTML的404,说明是Web服务层的问题;如果是数据请求404且页面框架正常,那更多是代理或API路径配置的问题,两者不要再混在一起排查。
6. 项目还能怎么扩展:进阶优化方向
6.1 从单体到微服务的演进思路
当前这个项目是一个标准的单体应用,这本身不是问题。但如果你打算拿它做毕业设计或者作为简历里重点展示的项目,我建议你多想想演进方向,面试官特别喜欢问这类问题:"如果用户量涨上去了,你会怎么拆分?"
合理的演进路径是:先把用户认证拆成独立的认证服务,再把新闻读写拆成内容服务,把评论拆成单独的评论服务,中间通过接口通信。这个拆分顺序的依据是业务优先级和团队结构,而不是一味追求微服务架构。单体架构在成本和运维上的优势非常明显,很多成熟产品早期都是单体,业务量上来后才逐步拆分。
6.2 缓存、全文搜索与对象存储的升级
新闻系统是典型"读多写少"的场景,首页热门新闻列表极适合加缓存。我实际做的优化是:门户端新闻列表接口查询Redis,如果缓存里没有再从MySQL查并把结果写入缓存,设置5分钟过期。这个优化上线后接口响应时间从平均200毫秒降到了50毫秒左右,效果非常明显。
搜索这个功能,小规模数据量用MySQL的LIKE查询就够,但数据量上来以后,LIKE前置百分号的查询无法走索引,会很慢。升级方案可以引入Elasticsearch做全文检索,也可以退而求其次用MySQL全文索引。对新闻系统来说,搜索是一个高频入口,投入在这块的价值很大。
图片存储也值得单独规划。开发期把图片存在本地磁盘没问题,但生产环境要有独立的对象存储方案。MinIO就是一个不错的选择,它部署简单、兼容S3协议,可以作为独立的图片服务。结合热词里很常见的"minio加入到springboot"这个搜索,这类需求确实在生产中很普遍。核心思路是:前端上传文件到后端,后端再把文件流转发给MinIO,返回一个可访问的URL。
6.3 安全加固与日志监控建议
最后聊点安全方面的经验。新闻管理系统如果直接暴露在公网,至少要盯住几个弱点:SQL注入、XSS脚本、暴力破解、越权访问。
SQL注入的防范,只要坚持使用MyBatis-Plus的预编译机制或者MyBatis注解SQL用#{}占位符,就能挡住绝大部分注入。XSS这块我在富文本部分已经提过,后端清洗必不可少。暴力破解的防范,可以在登录接口加上简单的图形验证码,或者对同一IP的失败次数做限制。越权访问是最隐蔽的:一定不要只校验"是否登录"就放行,还要校验"这个资源是否属于当前用户",编辑只能改自己的稿子这种逻辑必须在Service层落实。
日志这块我习惯的做法是:接口层打印请求路径和耗时,Service层打印关键状态流转,全局异常处理器记录error级别日志。不要小看这些日志,线上出问题时它们就是破案线索。监控方面可以引入Springboot Actuator,配合Springboot Admin做简单的健康检查和指标展示,这部分不需要换架构就能完成,是投入产出比很高的一件事。
最后分享一个我个人的维护习惯:每次改动完代码,先跑一遍核心链路——登录、发新闻、审核、发布、评论,确认无回归再提交。这个看似笨拙的办法,帮我挡掉了非常多次因为改了个"小地方"导致的线上事故。做这类管理系统,稳定性永远比新功能重要。