做这套系统之前,我所在的团队一直靠微信群发Word文档来走新闻稿件流转:编辑写完丢群里,审稿人看完批注再丢回去,最后发布时还得找排版人员手工复制粘贴。那段时间每天光对版本就够让人头大,更别说领导要某一篇稿件的完整审核记录时,根本无从查起。后来我干脆利用业余时间把前后端分离的新闻稿件管理系统从零搭了一遍,用SpringBoot扛接口、Vue做界面、MyBatis管数据库、MySQL存数据,把稿件从投稿、审核、改稿到发布归档的整条链路彻底串了起来。这篇博文就把整个系统的设计思路、核心模块实现方式和部署细节完整写出来,给正在做类似管理系统的开发者一份可以直接参照的实战参考。
1. 为什么自建稿件管理系统:散落文档带来的连锁麻烦
如果你觉得"做个系统"是小题大做,那大概率还没经历过几十篇稿件同时在微信里流转的混乱。我最初也没想动手写代码,直到连续踩了几次坑才意识到,问题已经严重到影响正常发稿节奏。
第一个坑是版本冲突。一篇稿件第一天改完发群里,第二天审稿人又基于旧版本提了修改意见,编辑基于错误版本又改了一版,最后发布出来的内容里居然残留着早该删掉的段落。第二个坑是审核留痕缺失。哪篇稿件谁审的、审了多久、提了什么意见,全凭聊天记录往回翻,领导问起根本答不上来。第三个坑是发布归档靠人肉。稿件发完以后没有统一归档入口,想要回顾历史稿只能一台一台电脑翻文件。
市面上当然有成型的OA系统,可以买来直接用。但对我们这种内部采编场景来说,那种大型系统的部署成本和学习成本都偏高,而且定制审批流程要额外排期。我自己在平时的开发中已经比较熟悉SpringBoot和Vue这套技术组合,四张表的业务复杂度也远没到需要微服务的程度,所以自己动手做一个"够用且能扩展"的轻量系统反而是性价比最高的方案。
这套系统上线跑通以后,最直接的收益是稿件流转完全可视化:编辑当天交了几篇稿、每篇稿处于什么状态、审稿人是否已处理、发布是否已完成,打开列表页一目了然。更重要的是审核记录自动留痕,每篇稿件哪个角色在什么时间做了什么操作都有据可查。
整体技术栈落在SpringBoot上做RESTful API,Vue负责页面渲染和交互,MyBatis承担SQL映射,MySQL作为持久化存储。这套组合在中小型管理系统中很常见,生态成熟、文档齐全、遇到问题很容易搜到答案,对个人开发者或小团队来说上手成本很低。
2. 技术选型背后的思考:为什么是这四件套
在动手写代码之前,我先花了两天时间把技术选型和架构设计定下来。这不是走形式,选型是否合理直接决定后面开发能不能顺畅推进。
2.1 SpringBoot:企业级开发的地基
后端框架选SpringBoot几乎没有犹豫。我实际开发中用过的框架不少,但论到快速搭建、生态成熟度和团队熟悉度,SpringBoot确实稳。对于新闻稿件管理系统这类典型CRUD加少量业务状态流转的应用,SpringBoot的自动配置机制能省掉大量XML配置,一个启动类加几个注解就能把Web环境跑起来。
具体版本上我选用了SpringBoot 2.7.x系列,对应的内置Tomcat容器、Spring MVC组件和Jackson序列化方案都比较成熟稳定。新闻稿件管理系统的接口主要是标准HTTP服务,不涉及复杂的WebFlux响应式场景,传统Servlet模型简单可靠,排查问题也直观。
2.2 MyBatis:理由绝对不是因为"老"
ORM选型时在MyBatis和MyBatis-Plus之间犹豫了一下。标题里写了MyBatis但没限定死,IDE里两个依赖都能引。最后我还是用了原生MyBatis,理由可能和很多人想的不一样。
新闻稿件管理系统的SQL虽然总量不大,但稿件列表查询涉及多表关联、状态过滤和分页排序,这类动态SQL在XML里写起来非常顺手。MyBatis的resultMap映射机制让我能够按需控制字段映射,前端需要的列表VO和数据库表结构能保持解耦。如果你打算扩充稿件全文检索功能,SQL调整起来也更直接。
有人可能会说MyBatis-Plus自带BaseMapper节省开发时间,这确实没错。但考虑到XML映射更便于后续SQL调优、动态条件拼装也更灵活,纯MyBatis对我这种习惯直接控制SQL的开发者更合适。如果你更看重开发速度、能把CRUD代码压缩到最少,选MyBatis-Plus也完全可以,业务代码层面差别不大。
2.3 Vue前端:组件化开发对上管理后台口味
前端选Vue 2.6 + Element UI 2.x的组合。后台管理系统最常见的交互是表格展示、表单填写、弹窗确认、标签页切换,这些场景Element UI都封装得很完备,组件风格统一,基本不用自己造轮子。
Vue 3和TypeScript当然更新,但当时的考虑是Element UI对Vue 2的匹配度非常成熟,编辑器端的各种封装组件和日期选择器在Vue 2环境里踩坑更少。如果你现在是从零开始的新项目,直接用Vue 3加Element Plus也没问题,核心组件代码逻辑是相通的。
前后端数据交互统一走axios,请求封装、拦截器、token注入都在一层统一处理。前端不做页面跳转时的数据拼装,所有数据都从RESTful接口获取,实现真正意义上的前后端分离。
2.4 MySQL存储:业务复杂度还没到需要很重的存储方案
数据库选MySQL 5.7版本。为什么不用Oracle或者PostgreSQL?新闻稿件管理系统最多也就几十个并发用户,数据量级在百万行以下,MySQL 5.7在性能和运维便利性上都很够用。5.7对JSON类型的支持也还可以,如果想给稿件扩展自定义字段,预留一个JSON列就能搞定。
字符集上我统一用utf8mb4,这是从一开始就定下来的红线。新闻稿件正文里难免有特殊符号,比如引号、破折号、手机号里的特殊字符,utf8mb4才能完整支持。排序规则选了utf8mb4_general_ci,日常查询大小写不敏感,符合中文内容的检索习惯。
3. 数据库设计:十九个字段如何撑起完整审核链路
数据库表设计是这套系统里最值得细说的一部分。整个系统只设计了7张表,但稿件状态的流转、审核记录的追踪、用户权限的控制,全靠这些表之间的字段设计来支撑。
3.1 核心表结构与字段设计逻辑
第一张核心表是users用户表。字段包括用户ID、用户名、密码密文、真实姓名、角色ID、部门ID、是否启用等。密码字段不存明文,用BCrypt加密存储,这是基础中的基础。用户名做唯一索引,登录时一次索引命中最快定位用户。
第二张是roles角色表,配合用户表实现简单的RBAC访问控制。系统设计了四种角色:管理员、编辑、审稿人、发布员。每种角色对应不同的操作权限,这个在后面接口鉴权时会详细讲。
第三张是categories栏目表,用于稿件归类。栏目支持两级结构,比如"综合新闻"栏目下可设"时政要闻"、“民生热点”等子栏目。栏目的做成分散在其他各表里,稿件引用栏目ID实现分类管理。
第四张是articles稿件表,这是核心中的核心。字段设计时重点思考了一个问题:查询高频字段要冗余,状态流转靠独立记录表。稿件表本身包含ID、标题、副标题、作者ID、栏目ID、正文内容、摘要、封面图URL、当前状态、是否推荐、发布时间、创建时间、更新时间等字段。
为什么把"当前状态"直接冗余在稿件表里?因为稿件列表页需要频繁按状态过滤查询,如果每次都要关联审核记录表才能算出最新状态,SQL复杂度会大幅上升。冗余一个status字段,列表查询直接WHERE status = ?,性能最优。而那这个状态是谁在什么时间改的,则通过独立的审核记录表来保证完整性。
第五张是review_records审核记录表,专门记录每一步审核流转。字段包括记录ID、稿件ID、审核人ID、审核动作(通过/驳回)、审核意见、审核时间。这张表设计成大量留档,不允许修改和删除,保证审核链路不可抵赖。
第六张是attachments附件表,管理稿件中的图片、文档附件。包含附件ID、稿件ID、文件原始名称、存储路径、文件大小、上传人ID、上传时间。稿件删除时通过稿件ID关联清理附件记录,避免孤儿数据。
第七张是operation_logs操作日志表,记录登录、稿件编辑、状态变更等关键操作。字段包括日志ID、操作人ID、操作类型、操作详情、操作时间、IP地址。这张表可以做系统审计,也可以配合后期大数据分析统计编辑工作量。
所有表统一使用自增ID做主键,不搞UUID。原因在于:InnoDB引擎下自增主键的B+树写入顺序友好,磁盘碎片少,查询性能和写入性能更稳定。这个量级的数据表不需要分布式ID生成方案。
3.2 稿件状态机的字段表达
稿件从录入到发布一共经历四个状态:草稿(0)、待审核(1)、已通过(2)、已驳回(3)。这个状态值直接存在articles表的status字段中。
状态机的流转路径如下:
- 编辑创建稿件后默认进入草稿状态,编辑可随时编辑修改。
- 编辑提交审核后,状态变为待审核,稿件进入审稿人待办列表。
- 审稿人点击通过,状态变为已通过,稿件进入待发布列表。
- 审稿人点击驳回,状态变为已驳回,稿件退回编辑修改。
- 发布员对已通过的稿件执行发布,状态变为已发布(4)。
实际操作中发现,仅仅在稿件表存一个当前状态不够用,比如审稿人驳回某篇稿件,编辑改完重新提交后,状态又从已驳回变为待审核——这时候审核记录表就起到关键作用了,每一步操作都追加一条记录,最终在稿件详情页可以按时间倒序展示完整审核时间线。
3.3 几组关键SQL演示
稿件列表查询是最典型的动态SQL场景。按标题模糊搜索、按栏目精确过滤、按状态过滤、按时间范围过滤,这些条件可能同时存在。MyBatis的动态SQL标签就派上用场了。
<select id="selectArticlePage" resultMap="ArticleVOMap"> SELECT a.id, a.title, a.column_id, c.name AS column_name, a.author_id, u.real_name AS author_name, a.status, a.create_time, a.publish_time FROM articles a LEFT JOIN categories c ON a.column_id = c.id LEFT JOIN users u ON a.author_id = u.id <where> <if test="keyword != null and keyword != ''"> AND a.title LIKE CONCAT('%', #{keyword}, '%') </if> <if test="columnId != null"> AND a.column_id = #{columnId} </if> <if test="status != null"> AND a.status = #{status} </if> <if test="startTime != null"> AND a.create_time >= #{startTime} </if> <if test="endTime != null"> AND a.create_time <= #{endTime} </if> </where> ORDER BY a.create_time DESC </select>审核记录查询也走多表关联,需要展示审核人姓名和时间,关联users表拿real_name。
<select id="selectReviewRecords" resultType="ReviewRecordVO"> SELECT r.article_id, r.review_action, r.review_comment, u.real_name AS reviewer_name, r.review_time FROM review_records r LEFT JOIN users u ON r.reviewer_id = u.id WHERE r.article_id = #{articleId} ORDER BY r.review_time ASC </select>两张SQL跑的都很稳定,因为所有关联字段都建了索引。articles表的status、column_id、author_id是高频查询字段,review_records表的article_id建立了普通索引,查询计划都走的索引查找,没有出现全表扫描。
4. 后端核心模块:登录鉴权到稿件流转的实现细节
后端代码按功能模块拆包,controller层只负责接收参数和返回结果,service层处理业务逻辑,mapper层通过MyBatis操作数据库。新闻稿件管理系统的后端核心模块主要集中在认证授权和稿件业务流上。
4.1 JWT登录态实现
登录接口收到用户名密码后,先对账号状态做前置校验,再用BCrypt校验密码。校验通过后,生成JWT令牌返回给前端。JWT的好处是服务端无状态,不需要在MySQL里建session表,分布式部署时也不存在session共享的问题。
@Service public class AuthServiceImpl implements AuthService { @Autowired private UserMapper userMapper; @Autowired private JwtUtil jwtUtil; @Override public LoginResult login(LoginRequest request) { User user = userMapper.selectByUsername(request.getUsername()); if (user == null) { throw new BusinessException("用户不存在"); } if (!BCrypt.checkpw(request.getPassword(), user.getPassword())) { throw new BusinessException("密码错误"); } if (user.getStatus() == 0) { throw new BusinessException("账号已禁用,请联系管理员"); } // 生成JWT,payload里只放用户ID和角色标识 String token = jwtUtil.createToken(user.getId(), user.getRoleId()); return new LoginResult(token, user.getRealName(), user.getRoleId()); } }JWT的密钥我放在application.yml里,通过环境变量注入,不写死在代码中。token过期时间设置的是12小时,采编岗一天工作时间内登录一次就够了,安全性和体验平衡下来这个时效最合适。
所有需要登录的接口统一走拦截器校验。拦截器先从Header里取Authorization字段,取出token后解析校验,通过后把用户信息放入ThreadLocal,供后续业务方法随时获取当前登录用户。
@Component 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.startsWith("Bearer ")) { throw new BusinessException("未登录或登录已过期"); } String realToken = token.substring(7); Long userId = jwtUtil.parseToken(realToken); if (userId == null) { throw new BusinessException("token无效或已过期"); } UserContext.setUserId(userId); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); } }拦截器注册时配置好拦截路径和放行路径,登录接口、静态资源路径放行,其余接口一律拦截。
4.2 RBAC权限控制:四个角色四套菜单
后端权限控制按照简单的RBAC模型实现。每种角色可以访问的API接口通过注解加拦截判断双重保障。
角色权限矩阵如下:
| 操作 | 编辑 | 审稿人 | 发布员 | 管理员 |
|---|---|---|---|---|
| 创建/编辑稿件 | 是 | 否 | 否 | 是 |
| 提交审核 | 是 | 否 | 否 | 是 |
| 审核稿件 | 否 | 是 | 否 | 是 |
| 发布稿件 | 否 | 否 | 是 | 是 |
| 用户管理 | 否 | 否 | 否 | 是 |
| 栏目管理 | 否 | 否 | 否 | 是 |
| 查看审核记录 | 是 | 是 | 是 | 是 |
实现层面,在需要校验角色的Controller方法上加自定义注解@RequireRole,然后在拦截器中解析当前用户角色,比对注解指定角色。如果只有编辑角色能调用的接口被审稿人请求了,直接返回403。这套实现比集成Spring Security的@PreAuthorize轻量,但阅读代码时角色规则也足够清晰。
@RequireRole({RoleConstant.EDITOR, RoleConstant.ADMIN}) @PostMapping("/submit") public Result submit(@RequestBody SubmitRequest request) { articleService.submitForReview(request.getArticleId()); return Result.success(); }4.3 稿件提交审核与状态流转
稿件提交审核是业务上最核心的接口。编辑创建稿件时状态为草稿,提交审核时后端要做几个检查:稿件标题不能为空、正文字数不能少于200字、当前状态必须是草稿或已驳回状态。不满足条件直接抛异常,前端根据错误码提示用户。
@Transactional public void submitForReview(Long articleId) { Article article = articleMapper.selectById(articleId); if (article == null) { throw new BusinessException("稿件不存在"); } if (article.getStatus() != ArticleStatus.DRAFT && article.getStatus() != ArticleStatus.REJECTED) { throw new BusinessException("当前状态不允许提交审核"); } if (article.getContent().length() < 200) { throw new BusinessException("正文字数过少,请补充内容后再提交"); } // 更新稿件状态 article.setStatus(ArticleStatus.PENDING_REVIEW); articleMapper.updateStatus(article); // 追加审核记录 ReviewRecord record = new ReviewRecord(); record.setArticleId(articleId); record.setReviewerId(null); // 审核人还未确定 record.setReviewAction("SUBMIT"); record.setReviewComment("编辑提交审核"); record.setReviewTime(new Date()); reviewRecordMapper.insert(record); }这个方法加了@Transactional,更新稿件状态和插入审核记录要么同时成功要么同时失败,不会出现稿件状态已是待审核但审核记录缺失的半截数据。
审稿人审核接口的处理流程类似,校验当前登录用户角色是审稿人,校验稿件状态是待审核,然后更新稿件状态并写入审核记录。审核通过时状态改为已通过,审核驳回时状态改为已驳回,并填写驳回原因。
4.4 附件上传的本地存储方案
新闻稿件系统免不了图片上传和文档附件。附件存储采用本地目录存储方案,没有引入OSS等云存储,主要考虑到系统服务于内网环境,数据不出边界更稳妥。
文件上传接口接收MultipartFile,文件类型做了白名单校验(图片允许jpg/png/gif,文档允许pdf/doc/docx),文件大小限制为10MB。存储路径按日期分目录,比如/data/uploads/2024/11/28/,文件名用UUID重命名避免冲突,同时把原始文件名记录到数据库。
public String uploadAttachment(MultipartFile file, Long articleId, Long uploaderId) { if (file.isEmpty()) { throw new BusinessException("文件不能为空"); } String originalFilename = file.getOriginalFilename(); String ext = FilenameUtils.getExtension(originalFilename); if (!ALLOWED_EXTENSIONS.contains(ext)) { throw new BusinessException("不支持的文件类型"); } if (file.getSize() > 10 * 1024 * 1024) { throw new BusinessException("文件大小不能超过10MB"); } String datePath = new SimpleDateFormat("yyyy/MM/dd").format(new Date()); String newFileName = UUID.randomUUID().toString().replace("-", "") + "." + ext; String relativePath = "/uploads/" + datePath + "/" + newFileName; String absolutePath = uploadRootDir + relativePath; // 确保目录存在 File destFile = new File(absolutePath); if (!destFile.getParentFile().exists()) { destFile.getParentFile().mkdirs(); } file.transferTo(destFile); // 记录到数据库 Attachment attachment = new Attachment(); attachment.setArticleId(articleId); attachment.setOriginalName(originalFilename); attachment.setStorePath(relativePath); attachment.setFileSize(file.getSize()); attachment.setUploaderId(uploaderId); attachment.setUploadTime(new Date()); attachmentMapper.insert(attachment); return relativePath; }上传目录放在服务器独立磁盘分区,不跟着项目工程走。这样一个是升级系统不会影响历史附件,另一个是备份时只需单独备份上传目录。
5. 前端交互实现:Vue组件如何承载采编流程
前端部分我按照"页面即模块"的思路拆分了几个核心视图:登录页、稿件列表页、稿件编辑页、审核详情页、栏目管理页和用户管理页。每个页面拆成可复用的子组件,避免单文件组件体积过大。
5.1 登录态处理和路由守卫
登录页的逻辑比较简单,表单校验通过后调用登录接口,拿到token后存入localStorage,接着跳转到稿件列表页。关键是路由守卫的设置,通过Vue Router的beforeEach导航守卫拦截未登录访问。
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (to.path === '/login') { next(); return; } if (!token) { next('/login'); return; } // 根据角色动态控制可访问页面 const roleId = Number(localStorage.getItem('roleId')); if (to.meta.roles && !to.meta.roles.includes(roleId)) { next('/403'); return; } next(); });axios请求封装时统一注入token,响应拦截器里处理401状态。每次请求携带Authorization: Bearer <token>,如果token过期后端返回401,前端自动清掉本地token并跳回登录页。
service.interceptors.response.use( response => { return response.data; }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token'); localStorage.removeItem('roleId'); router.push('/login'); } return Promise.reject(error); } );5.2 稿件列表页:筛选条件与分页联动
稿件列表页是整个系统的门面,编辑、审稿人、发布员每天打开系统首先看到的就是它。页面顶部是一排筛选条件:标题关键词输入框、栏目下拉选择、状态下拉选择、时间范围选择器,下面是表格和分页组件。
筛选条件变化时重置页码为1并重新拉取列表数据,列表接口的请求参数和工具栏状态通过watch同步。加载状态用loading变量控制,Element UI的表格自带loading属性,绑定即可。
每行操作按钮的显示逻辑设置得很细致,根据当前用户角色和稿件状态动态渲染。比如编辑看到自己创建的草稿稿件有"编辑""提交审核""删除"按钮;审稿人看到待审核稿件有"审核""查看文章详情"按钮;发布员看到已通过稿件有"发布"按钮。这套按钮逻辑是通过一个计算属性完成的,逻辑简单不易出bug。
5.3 稿件编辑页:富文本与附件上传
稿件编辑页是编辑角色使用频率最高的页面。标题和摘要用普通输入框,正文用富文本编辑器。富文本我采用的是基于tinymce封装的Vue组件,支持图片插入、表格、字体格式等功能,能满足一般新闻稿件的排版需求。
文本编辑器初始化和数据回显时要注意一个坑:Vue组件在异步请求到数据后,编辑器内容不会自动同步,需要调用编辑器的setContent方法手动赋值。我封装成子组件后暴露了loadContent方法,父组件拿到稿件详情后调用该方法完成回显。
附件上传区域遍历显示当前稿件的附件列表,支持在线预览图片和下载文档。上传时先请求上传接口拿回存储路径,再把路径和原始文件信息写入附件列表,一并随稿件数据保存。
<template> <div class="editor-container"> <input v-model="form.title" placeholder="请输入稿件标题" /> <tinymce-editor ref="editor" v-model="form.content" :height="400" /> <el-upload :action="uploadUrl" :headers="uploadHeaders" :on-success="handleUploadSuccess" multiple> <el-button type="primary">上传附件</el-button> </el-upload> <div class="attachment-list"> <div v-for="item in form.attachments" :key="item.id"> <a :href="item.storePath" target="_blank">{{ item.originalName }}</a> </div> </div> </div> </template>5.4 审核详情页:状态时间线一目了然
审稿人进入稿件审核页时,看到的是稿件完整信息加审核时间线。时间线组件放在页面的右侧栏,按照审核记录表的数据按时间正序渲染:创建草稿、提交审核、审稿通过、正式发布,每个节点显示操作人和操作时间,看起来非常清晰。
审核操作区有两个按钮:"通过"和"驳回"。驳回时必须填写审核意见,打开一个带textarea的Dialog,内容必填。通过操作不需要填写意见,但会弹出一个二次确认框,防止误点。
发布时间线数据完全从后端review_records表读取,不依赖前端缓存。这样即使多个浏览器同时打开同一篇稿件,看到的状态线索都是实时一致的。
6. 完整部署教程:从本机开发到服务器上线
部署这套系统我踩了不少坑,把整个过程整理成一份可以直接照着操作的清单,从环境准备到Nginx配置一条路走通。
6.1 服务器环境准备清单
部署前先确认服务器基础环境,我用的服务器是Linux CentOS 7.x系统,2核4G配置,跑这个系统绰绰有余。需要安装的组件如下:
| 组件 | 版本 | 用途 |
|---|---|---|
| JDK | 1.8 | 运行SpringBoot jar包 |
| MySQL | 5.7 | 数据存储 |
| Nginx | 1.20+ | 反向代理和前端静态资源服务 |
| Node.js | 14+ | 前端构建(仅构建时需要) |
| Maven | 3.6+ | 后端打包(仅构建时需要) |
服务器上需要开放三个端口:80端口用于Nginx对外服务,8080端口为SpringBoot应用提供接口服务(只支持内网访问,公网不直接暴露),3306端口为MySQL数据库服务(只允许内网访问)。
MySQL安装完成后,需要创建数据库和授权账号。字符集指定utf8mb4,授权账号只允许在服务器本机连接,避免数据库端口对外暴露造成安全风险。
CREATE DATABASE news_management DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER 'news_app'@'localhost' IDENTIFIED BY '你的数据库密码'; GRANT ALL PRIVILEGES ON news_management.* TO 'news_app'@'localhost'; FLUSH PRIVILEGES;6.2 后端打包部署步骤
后端工程在本地用Maven打包。
mvn clean package -DskipTests打包成功后在target目录下生成news-system.jar。把jar包上传到服务器,放在/opt/news-system/目录下。数据库初始化只需第一次部署时执行一次建表脚本,后续升级不用重复执行。
启动脚本start.sh内容如下:
#!/bin/bash APP_NAME=/opt/news-system/news-system.jar LOG_FILE=/opt/news-system/logs/app.log case "$1" in start) mkdir -p /opt/news-system/logs nohup java -Xms512m -Xmx102m -jar $APP_NAME \ --spring.profiles.active=prod \ > $LOG_FILE 2>&1 & echo "应用已启动,PID为: $!" ;; stop) PID=$(ps -ef | grep $APP_NAME | grep -v grep | awk '{print $2}') if [ -n "$PID" ]; then kill -9 $PID echo "应用已停止" else echo "未找到运行中的进程" fi ;; restart) $0 stop sleep 2 $0 start ;; *) echo "用法: $0 {start|stop|restart}" exit 1 ;; esac生产环境的配置文件application-prod.yml与本地dev配置分离,数据源地址、上传路径、JWT密钥等都通过环境变量注入,避免敏感信息写在仓库里。
spring: datasource: url: jdbc:mysql://localhost:3306/news_management?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai username: news_app password: ${DB_PASSWORD} file: upload-dir: /data/news-uploads jwt: secret: ${JWT_SECRET} expire-hours: 126.3 前端构建与Nginx配置
前端在本地执行npm install安装依赖,然后构建生成dist静态目录。
npm install npm run build构建前要确认vue.config.js中devServer的代理配置不影响build产物。生产环境下前端需要通过Nginx访问后端接口,配置一个反向代理即可解决跨域问题。
nginx.conf关键配置如下:
server { listen 80; server_name your-domain.com; root /opt/news-web/dist; index index.html; # 前端路由使用的history模式,请求不存在的路径时重定向到index.html location / { try_files $uri $uri/ /index.html; } # API请求反向代理到后端服务 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 上传文件访问路径映射 location /uploads/ { alias /data/news-uploads/; } }这里注意proxy_pass后面没有带路径,这样/api/article/list会被完整转发到后端接口。如果proxy_pass写成http://127.0.0.1:8080/,则会把/api前缀吃掉,后端接口路径对不上。
6.4 部署后常见问题排查
部署完成后总有些意料之中的小问题,把最常见的几个列出来供你对照:
问题一:前端页面打开正常,但列表接口提示404。大概率是Nginx代理配置不对。检查proxy_pass那行是否多写了斜杠,检查后端接口路径前缀——我的后端接口路径统一加了/api前缀,Controller上的RequestMapping是/api/article,这需要前后端约定好。
问题二:数据库连接报错Communications link failure。检查MySQL端口是否被防火墙拦截,检查数据库账号是否只允许localhost连接,检查URL里serverTimezone参数是否设置,时区不对偶尔会报连接异常。
问题三:图片上传成功但页面无法显示。检查Nginx配置中的location /uploads/的alias路径是否和实际上传目录一致。alias和root的区别要搞清楚,alias会把指定路径直接拼接在匹配的URI后面,配置错误最容易出现404。
问题四:JWT过期后页面跳转不了登录页。前端axios响应拦截器里判断401的逻辑要确保正确执行,且router对象在当前作用域可用。如果拦截器模块引用的router实例和Vue Router实例不是同一个,跳转会静默失败。
问题五:Tomcat默认线程池被打满了。通过nginx访问时如果并发量稍微上去一点,SpringBoot内置Tomcat的默认配置可能不够用。我在后端配置中单独调过线程池参数:
server: port: 8080 tomcat: max-threads: 200 min-spare-threads: 207. 个人实践总结与后续扩展建议
整套系统从需求分析到部署上线,前后花了两周时间,核心功能都是标准CRUD加状态流转换,开发路径很清晰。回顾这个项目,我觉得最有价值的部分不是代码量,而是对业务流程的把控:稿件状态怎么设计才能覆盖所有使用场景、审核记录怎么组织才能让追责有据可查、权限怎么划分才能让每个角色各司其职。这些业务侧的思考往往比技术选型本身更能体现一个开发者的成熟度。
有一点我想单独提醒:开发这类管理系统时,不要把时间浪费在"用最新技术"上,而要把精力花在"流程是否跑得通"上。我见过太多人把Spring Cloud全家桶堆上去,最后发现项目只有十几张表,微服务的复杂度完全没必要。就像这套系统,单体应用加一个良好的模块拆分,维护体验反而比分布式架构舒服得多。
部署上线之后,我还在持续优化几个方向。一个是给稿件列表增加全文检索能力,目前先用MySQL的LIKE查询顶着,数据量上来后可以集成Elasticsearch;另一个是给编辑角色增加个人工作量统计报表,用操作日志表按日期聚合就可以实现;第三是考虑接入对象存储替代本地附件存储,云上部署时更友好。
如果这篇内容能给你的技术选型或部署过程带来帮助,或者你觉得哪个模块还可以聊得更深,欢迎在评论区聊聊你的想法。作为开发者,项目落地那一刻的踏实感,比写多少行新代码都更让人满足。希望你的稿件管理系统也早日跑起来,让团队真正从"微信传文件"的地狱模式里解脱出来。