
这个项目是我今年做完的一套新闻稿件管理系统后端用的是 SpringBoot MyBatis MySQL前端是 Vue 3 Element Plus Vite典型的前后端分离结构。网上打SpringBootVueMyBatisMySQL源码标签的项目一大把但多数要么是烂大街的 CRUD 示例要么版本混乱到让人看着就想删库跑路。这篇博文不打算复述系统说明书我想把做这个项目过程中真正有价值的决策逻辑、关键代码写法、部署流程以及踩过的十几个坑一次讲清楚保证你拿到的是一套能跑、能改、能上线的完整工程而不是一个只能停留在 README 里的半成品。你如果是个刚接触前后端分离项目的新手这个项目是一个特别合适的练手对象因为它把用户登录、权限拦截、稿件 CRUD、状态流转、审核流这些常见业务都串进去了难度又控制在全栈入门到进阶的区间。如果你已经有 Java Web 基础想把 SpringBoot 和 Vue 的联调细节、部署细节弄扎实这篇文章可以直接当参考手册用。我在整理的时候会把每个关键选择背后的原因也交代一遍遇到参数和配置都会给出实际值尽量让你不用再翻第二篇资料。1. 项目整体设计与技术选型思路1.1 为什么坚持用前后端分离架构做新闻稿件管理系统这种典型的内部业务系统最早的方案往往是 JSP 或者 Thymeleaf 模板渲染后端写好一个页面改一行样式都得重新编译重启前端工程师和后端工程师在一个项目里互相等协作成本很高。前后端分离之后前端只负责页面和交互后端只负责提供 RESTful API两边并行开发互不阻塞部署也能各自独立扩展。这个项目里我明确把前后端边界定在接口契约上后端统一返回{ code, message, data }格式前端所有的请求都走一个封装好的 axios 实例业务层不关心页面长什么样页面也不直接拼 SQL。这样做的好处是后期如果想把前端从 Vue 换成 React或者把后端接成微服务核心业务逻辑都不用推翻重来。很多人在群里问若依前后端分离前端代码在哪里这类问题本质就是没搞明白前端工程和后端工程是独立的两个代码仓库。我这个项目也是同样的结构news-admin-server是后端 SpringBoot 工程news-admin-web是前端 Vue 工程两个目录互不嵌套前端代码只在打包后放到 Nginx 里或者开发时通过代理转发到后端端口。1.2 技术栈选型的背后考量后端选 SpringBoot主要是看中它的自动装配和 starter 机制一个新闻稿件管理系统不需要像传统 SSM 那样写一堆 XML 配置。数据库用 MySQL 5.7 版本稳定、教程多、资料好找生产中大量系统还在用这个版本对新手最友好。持久层用 MyBatis 而不是 JPA是因为稿件列表这种页面经常要写多表关联、条件动态拼接和审核统计查询直接用 SQL 控制更直观也更好排查性能问题。前端用 Vue 3 配合 Vite组件化开发维护成本低Element Plus 做后台管理界面几乎开箱即用。版本选择这里有个容易踩的坑我单独说一下如果你搜教程下载依赖时发现 SpringBoot 已经是 3.x千万别直接拿来和 MyBatis、旧版包管理混用。SpringBoot 3.x 把javax换成了jakarta很多老版本的 MyBatis starter、第三方工具类都会直接报错。这个项目我锁定的是SpringBoot 2.7.18mybatis-spring-boot-starter 2.3.2兼容性经过了大量验证你照着配基本不会出幺蛾子。别嫌版本低做业务系统稳定压倒一切。1.3 新闻稿件管理系统的核心业务域划分拿到需求之后我没有急着写代码而是先把业务域画了一遍。这个系统本质上解决的是几个问题记者提交稿件、编辑初审、主任终审、发布上线、下线归档。围绕这条链路我抽象出四个核心模块用户与权限、分类管理、稿件管理、审核流程。角色我设计了三种管理员负责用户管理和分类维护编辑负责稿件的编辑和提交审核审核员负责审核和发布。稿件状态用数字字段表示0草稿1待审核2审核通过3已驳回4已发布5已下线。这个状态机贯穿整个系统的核心逻辑前端列表页的按钮显隐、后端接口的权限校验全部围绕这个状态字段控制。2. 后端核心模块拆解与实现要点2.1 工程结构规划与项目初始化后端工程我按标准的 Controller-Service-Mapper 三层去组织额外加了config、common、dto、entity四个包。config放拦截器、跨域配置这类东西common放统一返回结果、异常处理、工具类dto放接口入参对象避免直接用实体类接收前端传参。初始化项目我用的是 IDEA 自带的 Spring Initializr选 Java 8 语言级别依赖先只勾 Spring Web。剩下的 MyBatis、MySQL、Lombok、JWT 相关依赖我在pom.xml里手动补上关键依赖如下dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency这里要提醒一下jjwt 0.9.1依赖的jaxb-api在某些 JDK 版本下会缺失建议同时加上javax.xml.bind:jaxb-api:2.3.1否则运行时容易报ClassNotFoundException。这个问题在 JDK 11 以上特别常见早加早省心。2.2 用户登录与 JWT 鉴权流程登录接口的逻辑不复杂前端把用户名密码传过来后端先通过BCryptPasswordEncoder校验密码校验通过后生成一个 JWT token 返回给前端前端把它存在 localStorage之后每次请求在Authorization请求头带上。JWT 的核心价值在于让鉴权变得无状态不用像 Session 那样在后端存会话信息。它的结构分三段Header 存算法类型Payload 存用户信息比如 userId、roleId、过期时间Signature 用密钥签名。后端拦截器每次请求都解析这个 token能解析出来就放行解析失败直接返回 401。我的拦截器实现思路是这样的public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || token.isEmpty()) { response.setStatus(401); return false; } try { Claims claims JwtUtil.parseToken(token.replace(Bearer , )); request.setAttribute(userId, claims.get(userId)); request.setAttribute(roleId, claims.get(roleId)); return true; } catch (Exception e) { response.setStatus(401); return false; } } }然后注册进WebMvcConfigurer把需要放行的路径比如登录接口/api/auth/login明确排除在拦截范围之外。实测中很多人忘记处理Bearer前缀导致解密时拿到带空格的字符串解析直接失败。2.3 核心数据表设计与状态字段规划数据库是整个系统的地基我设计了四张核心表sys_user用户表、news_category分类表、news_article稿件表、article_audit_log审核日志表。稿件表的设计我花了比较多精力因为它要兼顾查询效率和业务扩展。一张简化版的建表语句是这样的CREATE TABLE news_article ( id bigint(20) NOT NULL AUTO_INCREMENT, title varchar(200) NOT NULL COMMENT 稿件标题, summary varchar(500) DEFAULT NULL COMMENT 稿件摘要, content longtext COMMENT 正文内容, cover varchar(255) DEFAULT NULL COMMENT 封面图URL, category_id bigint(20) DEFAULT NULL COMMENT 分类ID, author_id bigint(20) DEFAULT NULL COMMENT 作者用户ID, status tinyint(4) DEFAULT 0 COMMENT 0草稿 1待审核 2通过 3驳回 4发布 5下线, view_count int(11) DEFAULT 0 COMMENT 浏览量, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted tinyint(1) DEFAULT 0 COMMENT 逻辑删除, PRIMARY KEY (id), KEY idx_status (status), KEY idx_category_status (category_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT新闻稿件表;deleted字段做逻辑删除是最基本的约定新闻稿件有审核记录物理删掉会留下烂账所以一律用0/1标记。索引方面我建了idx_status和联合索引idx_category_status因为最频繁的查询就是按分类和状态过滤列表这个联合索引能让覆盖查询少回表实测数据量到几十万之后差别很明显。2.4 MyBatis 配置与动态 SQL 实战MyBatis 的配置我放在application.yml里核心就几个参数mapper 文件路径、实体类别名、驼峰映射、SQL 日志。这里单独提一下驼峰映射如果你表字段是create_time实体属性是createTime必须开启map-underscore-to-camel-case: true否则查出来的对象属性全是 null。mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.news.admin.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl稿件列表页的查询条件非常多分类、状态、标题关键词、时间范围都是可选的用注解写 SQL 会把人逼疯。我在ArticleMapper.xml里用动态 SQL 处理核心片段如下select idselectArticlePage resultTypecom.news.admin.entity.Article SELECT * FROM news_article where if testtitle ! null and title ! AND title LIKE CONCAT(%, #{title}, %) /if if testcategoryId ! null AND category_id #{categoryId} /if if teststatus ! null AND status #{status} /if AND deleted 0 /where ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} /select分页我手写 LIMIT没有引入 PageHelper。原因是这个项目查询量可控手写 LIMIT 直观且避开 PageHelper 与 MyBatis 版本不兼容的坑。#{offset}是预编译占位符不存在 SQL 注入风险但如果你图省事改成${offset}用户传个恶意参数进来就直接被拼接进 SQL这属于绝对不能碰的禁令。3. 前端 Vue 项目搭建与页面实现3.1 Vue 环境准备与项目初始化前端工程我用了 Vite 构建创建命令是npm create vitelatest news-admin-web -- --template vue。开始之前先确认 Node.js 版本我建议用 Node 16 或 18Vite 对 Node 版本有要求太低跑不起来太高又容易和旧依赖产生兼容问题。整个前端依赖安装过程很多新手会在npm install上卡住。一个实用建议是先把 npm 源切到国内镜像再装依赖不然node_modules下载到一半超时是常态。执行一次npm config set registry https://registry.npmmirror.com之后再重新安装速度明显不一样。还有人问vue项目源码怎么发给别人这里顺带说一句node_modules绝对不要发也不要打进压缩包里面别人拿到手直接npm install就能还原源码包的大小能少掉 90%。Vue 项目建好之后我先把目录按功能划分好views放页面组件、api放接口调用、router放路由配置、utils放 axios 封装每个模块单独一个文件夹。这一步看起来简单但对后期维护影响非常大我见过不少项目所有组件堆在一个目录里找文件要找半天这种工程习惯千万别学。3.2 路由设计与导航守卫后台管理系统的路由和普通官网不一样不能把所有页面全部平铺开必须考虑到登录态和权限。我设计了两种路由公开路由和需要登录的路由。登录页是公开的稿件列表、编辑、审核页都挂在一个叫Layout的父路由下面布局组件负责渲染侧边栏和顶部栏。效果最好的做法是在路由配置里给每个页面加meta元信息标角色权限比如meta: { roles: [admin, editor, auditor] }。然后在路由守卫beforeEach里统一判断router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) { next() } else if (!token) { next(/login) } else { next() } })这段逻辑看着简单但解决了一个实际问题没有登录的人无论怎么修改地址栏 URL都进不了系统内部页面。路由参数方面列表页跳详情页我用的是query方式比如router.push({ path: /article/detail, query: { id: row.id } })这样即使刷新页面参数也还在 URL 上不会丢。3.3 核心页面拆解与组件化实践前端页面里工作量最大的是稿件列表页。我用 Element Plus 的el-table渲染数据搭配el-pagination做分页顶部放搜索表单。每次搜索或翻页时把参数传给getArticleList接口后端返回分页结构再填回表格。这个页面虽然数据量不大但接口调用频率最高我把组件的加载状态、空数据状态都处理了一遍用户体验才过得去。稿件编辑页是另一个重点新闻正文不能像普通表单那样只用一个el-input我接了 wangEditor 富文本编辑器通过v-model双向绑定富文本 HTML。这里有个小经验富文本编辑器生成的内容要存储成 HTML后端用longtext字段存没问题但是富文本里的图片如果直接转 base64数据库会瞬间膨胀最好前端先把图片上传到服务器换回 URL正文里只存图片地址。这个优化做完我的库体量小了将近三分之一。审核页面相对简单审核员打开稿件详情看到正文点通过或驳回。驳回的时候必须填原因原因会写入article_audit_log表并回传给作者。这条记录链很重要编辑可以说清楚这篇稿子为什么被退回来否则审核流程就是一个黑盒业务上完全站不住脚。3.4 Axios 封装与开发环境跨域处理前端所有接口请求我都封装在一个 axios 实例里baseURL 设成/api它允许我在开发和生产环境用同一套代码。拦截器里做了两件重要的事从 localStorage 取 token 加到请求头响应收到 401 时自动清掉登录状态并跳回登录页。service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) service.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token) location.href /login } return Promise.reject(error) } )开发环境最头疼的跨域问题我用 Vite 代理解决。在vite.config.js里加一段配置server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求/api/article/list时Vite 开发服务器会帮它转发到后端的http://localhost:8080/api/article/list浏览器视角里请求是同源的就不会触发 CORS 报错。生产环境则靠 Nginx 做同样的反向代理两边的规则几乎一样切换环境毫无压力。4. 数据库配置与部署上线全流程4.1 MySQL 安装、初始化与远程访问设置部署第一步是搞定数据库。Windows 上装 MySQL 5.7.44 我踩过不少无谓的坑安装时下载的是 zip 解压版那就要手动写my.ini配置文件并执行mysqld --initialize-insecure初始化数据目录然后mysqld --console启动服务。如果你下载的是安装版安装向导里有一个步骤是选开发者模式还是服务器模式很多人找不到develop那个选项其实它对应的就是 setup type 选择页你要是只在本机开发调试选Developer Default就行但要记得把Install As Windows Service勾上这样开机不用手动启动数据库。库建好之后我用 root 登录执行初始化脚本。先创建一个独立用户而不是直接用 root 连业务库这个习惯能避免很多安全风险CREATE DATABASE IF NOT EXISTS news_manager DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER news% IDENTIFIED BY News123456; GRANT ALL PRIVILEGES ON news_manager.* TO news%; FLUSH PRIVILEGES;utf8mb4是必须的它可以完整支持四字节 Emoji 字符和生僻字新闻稿件正文里出现这种内容并不罕见。远程访问方面如果你本机连不上云服务器的 MySQL第一步检查防火墙和安全组是否放行了 3306 端口第二步检查 MySQL 的 bind-address 是否设置成了0.0.0.0。有读者问过怎么访问 docker 容器内的 MySQL其实原理相同启动容器时用-p 3306:3306做端口映射宿主机外面就拿它当普通 MySQL 用。4.2 后端打包与 Jar 包部署后端打包我用的 Maven。在项目根目录执行mvn clean package -DskipTests构建完成后target目录下会生成一个news-admin-server-0.0.1-SNAPSHOT.jar。这个 jar 包自带内嵌 Tomcat不需要额外装 Tomcat 容器。我一般把 Jar 包传到服务器/opt/news目录然后执行启动命令java -jar news-admin-server-0.0.1-SNAPSHOT.jar \ --spring.profiles.activeprod \ --server.port8080按CtrlC会直接停掉进程正式部署肯定不能这样干。我用nohup把它放到后台运行nohup java -jar news-admin-server-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod /opt/news/app.log 21 启动完成后看日志最直接的方法是tail -f /opt/news/app.log看到Started Application in x seconds就是启动成功。如果端口被占用用netstat -tlnp | grep 8080查进程把它 kill 掉再重启。后端的数据库密码我放在application-prod.yml里用环境变量覆盖这样源码即使传到代码仓库不暴露生产凭据。配置项大概是这样spring: datasource: url: jdbc:mysql://${MYSQL_HOST}:3306/news_manager?useUnicodetruecharacterEncodingutf8useSSLfalse username: ${MYSQL_USERNAME} password: ${MYSQL_PASSWORD}启动命令里加上环境变量参数即可比如MYSQL_USERNAMEnews MYSQL_PASSWORDNews123456 java -jar xxx.jar。Jar 包动态读环境变量换环境不用改代码这个模式我觉得是单机部署最省心的方案。4.3 前端打包与 Nginx 配置前端打包很简单项目根目录执行npm run build构建产物会生成到dist目录里面就是纯静态的 HTML、JS、CSS。打包之前记得把接口地址配置从开发模式切到生产模式我在.env.production里设置了VITE_API_BASE_URL/api因为生产环境 Nginx 会通过这个路径代理到后端。我习惯把dist整个目录丢到服务器/usr/share/nginx/html下然后修改 Nginx 配置文件核心 server 块如下server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files这一行是前端路由刷新不 404 的关键不懂它的人上线后刷新页面就看到白屏或 404。原理就是 Nginx 发现请求路径对应的文件不存在时就把路由交给index.html让 Vue Router 自己决定渲染哪个页面。修改完配置执行nginx -s reload即可生效。4.4 日志切分与常见运维技巧业务系统上线之后日志是排查问题的主要手段。SpringBoot 默认输出到控制台重定向到文件之后如果不切分文件会越来越大。我在application-prod.yml里配置了 Logback 滚动日志logging: file: name: /opt/news/logs/app.log logback: rollingpolicy: max-file-size: 10MB max-history: 7这样单个日志文件超过 10MB 会自动切割最多保留 7 个历史文件。这个配置非常轻量不会引入额外依赖却能在半年之后帮你省下清理磁盘的时间。前端部署方面如果有 CDN 加速需求把静态资源指到 OSS 或 CDN 域名Nginx 配置里对应调整location的 root 路径就行改动量很小。5. 常见问题与踩坑记录5.1 SpringBoot 版本过高引发的兼容性问题标题热搜里有一条springboot版本太高这绝对是我的真实经历。有段时间我图新鲜在新项目里试了 SpringBoot 3.1结果发现原来用的mybatis-spring-boot-starter直接起不来一堆java.lang.ClassNotFoundException: javax.servlet.Filter报错砸到脸上。因为 SpringBoot 3.x 只支持jakarta.*命名空间老包里的javax.*全部失效。后来我学乖了凡是做这类管理系统一律锁版本。官方推荐组合是 SpringBoot 2.7.18 MyBatis starter 2.3.2 MySQL Connector 8.0.33这套组合我跑了两个项目都没有出现兼容问题。如果你非要上 SpringBoot 3.x那就必须用mybatis-spring-boot-starter3.0 以上并且所有import javax.*全换成import jakarta.*代码改动量很大业务项目不建议做这个迁移。5.2 前端依赖安装与构建失败实录前端踩坑集中在依赖安装阶段。第一个高频报错是node-sass安装失败这个老牌依赖在新 Node 版本下编译特别容易挂。我的方案是直接用sass替代或者干脆降级 Node 版本到 16。第二个高频问题是vue-tsc类型检查太严格导致打包失败实际上很多小项目根本没必要做全量类型检查我在构建脚本里把vue-tsc去掉了只保留vite build构建时长直接缩短一半。还有一个实用建议如果npm install的时候某个包反复报错试试先删掉node_modules和package-lock.json再重新安装。锁文件有时会和当前 Node 版本产生内部依赖冲突重装能解决大部分诡异问题。把package.json与锁文件一起提交到仓库是个好习惯别人克隆之后可以精确重现你的依赖环境。5.3 跨域、Token 失效与请求死循环开发时最常见的报错是控制台出现Access to XMLHttpRequest has been blocked by CORS policy。这个问题的本质是浏览器的同源策略挡住了跨域响应解决思路只有两种后端配CrossOrigin或全局 CORS 过滤器前端通过 Vite 代理把请求转成同源。我这里强烈建议用代理方式因为后端 CORS 配置在正式环境还需要再调一遍代理方案对环境切换最友好。另一个非常隐蔽的坑是 token 过期之后axios 拦截器里做了跳转/login的处理但登录页本身也会请求接口结果跳转请求又带上了过期的 token后端又返回 401形成死循环。我的解决方式是在跳转前先判断当前路由是不是/login同时清理失效状态避免重复触发。这个 bug 排查了一个多小时最后发现就是差这一行判断。5.4 MyBatis 缓存、空值与接口绑定问题MyBatis 的缓存问题在面试题里经常出现实际开发也要注意。一级缓存是 SqlSession 级别的默认开启同一个事务内重复查询会命中缓存二级缓存是 mapper 级别的需要配置cache标签。新闻管理系统里稿件数据实时性要求高我没有开二级缓存因为一旦稿子状态更新缓存里的旧数据还在审核员看到的状态严重滞后这种脏数据事故宁可不要那点性能收益。联调过程中还碰到过一个典型问题前端传了一个空字符串title给后端XML 里的if testtitle ! null and title ! 能正确跳过条件但有人写testtitle ! null结果空字符串同样拼进 SQL查出来空结果集。另外 mapper 接口绑定不对的报错也经常出现com.news.admin.mapper.ArticleMapper必须跟 XML 文件的 namespace 完全一致路径大小写错了或者少个包名都会报Invalid bound statement排查方法就是先看 namespace 再看mapper-locations配置。6. 二次开发与扩展建议6.1 从新闻稿系统扩展成通用 CMS 平台这个项目的边界可以很自然地扩大。比如把稿件表抽象成内容表加一个content_type字段区分新闻、公告、活动页就变成通用内容管理平台。再比如增加栏目层级把news_category表改成父子结构加一个parent_id字段就能支持栏目树形管理很多政府网站的后台就是这么做的。扩展的时候有一个原则必须守住宁可扩展表结构不要滥用字段。最常见的设计错误是给稿件表加一批业务无关的扩展列比如加个is_top、is_hot后期每个新需求都加一列表结构最后烂到没法维护。正确的做法是单独建关联表或 JSON 扩展字段把个性化需求和核心字段隔离保证核心查询路径永远是稳定的。6.2 权限控制从按钮级到接口级目前系统的权限控制是角色判断 路由守卫颗粒度比较粗。如果要面向更多内部角色建议把权限细化到按钮权限和接口权限。前端通过后端的用户信息接口拿到当前用户有权限的按钮标识数组渲染时就v-ifhasPermission(article:delete)控制按钮显隐后端则在拦截器里校验接口路径对应的权限码防止有人通过 Postman 直接调删除接口绕过前端。这套方案做起来不难重点是权限码的命名规范要统一。我建议按模块:操作的模式比如article:add、article:audit、user:resetPwd这样一个权限码就能同时映射到前端按钮和后端接口维护成本很低。6.3 编辑器选型与多媒体处理富文本编辑器选型我有发言权wangEditor 上手简单适合新闻稿录入场景官网文档齐全中文搜索资料也多。如果后续需要更复杂的排版可以换成 TinyMCE 或 Quill这两个编辑器功能更强插件的生态也更完善。无论选哪个都要注意 XSS 防护富文本内容如果不过滤就存库并展示等于给攻击者留了注入脚本的口子。过滤方案用js-xss这类白名单过滤库在保存之前把script、onerror这类危险标签去掉。视频稿件功能如果要做记得不要直接把视频文件塞进 MySQL数据库存文件的 URL文件本身放到 Nginx 静态目录或者对象存储里。播放格式要考虑浏览器兼容性MP4 是最保险的选择其他格式需要转码服务规划的复杂度会上升不少。这块我建议先做图片封面和图文混排视频能力作为二期再扩展。6.4 我项目跑顺后的三点体会整套流程走完我最大的体会是不要再纠结最潮的技术栈新闻稿件管理系统这类项目核心是业务状态流转清晰、权限边界明确、部署链路稳固。SpringBoot 2.7 Vue 3 MyBatis MySQL 这套组合看着不够性感但它组合成本低、社区资料多、排查问题容易上线半年没出过让人睡不着觉的事故。还有一点是关于源码分发的建议。这个项目虽然叫完整源码但真正让别人快速跑起来的关键是写好初始化 SQL 和 README包括数据库密码、启动端口、前端代理地址都要写清楚。我见过太多人发源码的时候只发一个压缩包别人解压之后光配数据库就要折腾半天体验非常差。最后说一个隐藏小技巧如果你做的是类似的毕业设计或公司内部工具可以把统一返回结构{code, message, data}中的 code 定义成一套枚举比如200成功, 401未登录, 403无权限, 500系统异常前后端都照这套枚举做判断。别嫌它土前端少写一大堆 if else联调的时候双方还能对着错误码迅速定位问题比任何骚操作都管用。这个项目扩展道路很长先把最基础的地基打牢后面加什么都顺。