恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
新闻管理系统实战:从数据库设计到安全上线的完整指南
首页
资讯中心
/
新闻管理系统实战:从数据库设计到安全上线的完整指南
新闻管理系统实战:从数据库设计到安全上线的完整指南
发布时间:2026/10/6 6:57:29
简介围绕软件综合实习题目整理的新闻管理系统完整设计文档面向计算机、软件工程及信息管理相关专业学生尤其适合正在完成课程设计、综合实习或毕业设计的人群可帮助理解新闻发布类系统从需求分析、功能设计到模块落地的整体思路。资源共1个文件为doc格式文档压缩包整体约77KB虽篇幅精简但结构完整便于查阅与二次编辑。已有534人浏览学习。文档围绕系统概述与需求分析展开详细说明前台新闻分类导航、站内模糊搜索、分类内容展示、友情链接以及后台管理员设置、新闻信息管理、友情链接管理等功能设计并配有功能结构图与操作界面说明。整份文档既可作为系统设计说明书的写作范本也可为后续自主开发同类管理系统提供功能规划、模块划分与文档组织参考。1. 新闻管理系统不是“增删改查”那么简单它真正在考验什么你在简历里写“独立完成新闻管理系统”的时候毕业答辩和面试官最容易顺着这个标题追问下去同一秒有几百个用户打开详情页浏览量字段还能不能扛住并发后台编辑发布的新闻带了一堆富文本脚本为什么线上用户一访问就弹窗这些问题问的不是你写了多少接口而是你有没有把“内容发布、登录态、发布状态机”这三件事想透。这类系统看起来是标准的信息管理后台但它的真正价值在于那条从“编辑录入”到“访客看到”的完整链路——权限、状态、内容安全、发布时间、浏览量统计任何一个环节做成摆设系统都只算“能跑”不算“能用”。这篇文章就按我实际交付这类项目的顺序把数据模型、登录拦截、前台渲染和上线排错一次讲透适合正在做毕业设计、课程设计或者刚接手公司内容后台练手的新手。2. 先定数据模型再写代码新闻管理系统的核心表结构与 SQL2.1 三张基础表的设计news、category、user 的字段取舍做新闻管理系统最常见的做法是围绕三张核心表展开新闻表、分类表、用户表。很多新手一开始会把字段堆得很满什么“点击量”“点赞量”“来源URL”全塞进去结果写代码时自己都被绕晕。我一般会先按业务动作来划字段这条新闻要经过“写草稿—提交审核—发布”的状态流转所以必须要有状态字段后台首页要按分类筛选所以要有分类ID列表页要显示封面图和摘要这两个字段也必须落表不能每次动态截取正文否则列表页会慢。news 表里除了 title、content、cover_image 这些明面上的字段有几个很容易被漏掉但非常关键的summary摘要、status状态、is_top是否置顶、published_at实际发布时间。summary 的长度建议设到 300 到 500 个字符列表页直接查它避免一次性把大字段 content 全捞出来。status 我用 0、1、2 三个值分别表示草稿、已发布、待发布而不是用布尔值因为定时发布功能需要“待发布”这个中间状态。published_at 和 created_at 分开因为编辑可以先把文章保存为草稿过几天才真正发布列表排序应按照 published_at而不是创建时间。user 表相对简单username、password、nickname、role 四个字段就够用。role 用 0 表示管理员、1 表示编辑不需要设计成复杂的 RBAC 表——新闻后台的权限场景就是“谁能发新闻、谁能管分类”一个角色字段足够过度设计反而增加理解成本。2.2 冗余字段与查询效率status、is_top、comment_count 为什么不能省写系统最忌讳的是“省字段跑通后再补”。news 表里我坚持保留 comment_count 和 view_count 两个计数冗余字段虽然它们严格来说违反了第三范式但实操中收益远大于代价。如果你用 count(*) 去统计某条新闻的浏览量数据量到十万级时详情页每次刷新都会产生一次全表聚合查询这种消耗完全没有必要。正确做法是让详情页只做一次 update 自增列表页直接查这个字段值。有一个血泪经验是is_top 必须单独建字段而不能用 published_at 排序模拟置顶。因为置顶是一段有期限的动作运营说“这条置顶三天”到期你还得写个定时任务去把数据刷回来更麻烦。is_top 只是替代的做法是前台列表 order by is_top desc, published_at desc——这个排序条件里is_top 用 TINYINT(1) 存 0 或 1索引效果虽然一般但胜在语义直白。category 表里有一个 sort_order 字段它决定了后台编辑框里分类下拉框的排序。用 TINYINT数字越小排越前千万别直接用 varchar 存“1,2,3”。否则以后要在第一个分类前插入新分类时要把后面所有分类编号都改一遍属于典型的给自己挖坑。2.3 建表 SQL 与初始化数据从零跑通的最短路径下面这套建表 SQL 是按 MySQL 8.x 写的如果你用的是 MySQL 5.7只需要把 utf8mb4_0900_ai_ci 改成 utf8mb4_general_ci 就行。我的建议是字符集统一用 utf8mb4而不是 utf8否则用户粘个 emoji 进新闻正文保存时直接报“Incorrect string value”的错误。CREATE DATABASE IF NOT EXISTS news_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE news_system; CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 分类名, sort_order TINYINT NOT NULL DEFAULT 0 COMMENT 排序越小越靠前, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0停用, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT新闻分类表; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT BCrypt哈希, nickname VARCHAR(50) DEFAULT COMMENT 显示昵称, role TINYINT NOT NULL DEFAULT 1 COMMENT 0管理员 1编辑, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0锁定, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT后台用户表; CREATE TABLE news ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL COMMENT 标题, summary VARCHAR(500) DEFAULT COMMENT 摘要, content MEDIUMTEXT COMMENT 正文HTML, cover_image VARCHAR(500) DEFAULT COMMENT 封面图URL, category_id INT NOT NULL COMMENT 分类ID, status TINYINT NOT NULL DEFAULT 0 COMMENT 0草稿 1发布 2待发布, is_top TINYINT NOT NULL DEFAULT 0 COMMENT 0不置顶 1置顶, view_count INT NOT NULL DEFAULT 0 COMMENT 浏览量, comment_count INT NOT NULL DEFAULT 0 COMMENT 评论数站外冗余, publisher_id INT NOT NULL COMMENT 发布人ID, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, published_at DATETIME DEFAULT NULL COMMENT 实际发布时间, KEY idx_category_status (category_id, status), KEY idx_published_at (published_at), CONSTRAINT fk_news_category FOREIGN KEY (category_id) REFERENCES category(id) ) ENGINEInnoDB COMMENT新闻主表;这段 SQL 里我特意在 news 表上加了一个联合索引 idx_category_status。原因很直接后台管理列表最常见的操作是“按分类筛选 状态筛选”管理页又要按 published_at 倒序排列。这个索引能让筛选和排序在数据量大时依旧稳定。comment_count 之所以说是“站外冗余”是因为真正的评论表如果有的话会在插入评论的事务里同步更新这个字段取列表时就不用再做聚合查询。初始化数据时至少要插入一个管理员账号和一个默认分类。密码这里先用 BCrypt 加密的占位符后面讲权限控制时会给出完整的注册/登录代码这里只需要确保 status 是 1默认分类的 status 也是启用状态。插入后用SELECT LAST_INSERT_ID();确认分类的主键值然后拿它去插新闻数据避免外键约束报错时不知道是哪个表的问题。3. 后台登录与权限控制为什么你的 Session 校验等于没写3.1 登录逻辑的最小实现从登录接口到 Session 写入新闻管理系统的后台登录我见过不少新手项目是前端跳过一个登录页直接拿 localStorage 存个 username 就算登录了这种方案在企业内部项目里都会被否掉。正确做法是后端在登录成功后把用户信息写入 Session同时在每次访问受保护接口时校验这个 Session 是否存在。Java 方向的实现中登录接口的核心逻辑只有三步按用户名查库、BCrypt 校验密码、把用户对象塞进 Session。PostMapping(/admin/login) public String login(String username, String password, HttpSession session, HttpServletRequest request) { User user userMapper.findByUsername(username); if (user null || !BCrypt.checkpw(password, user.getPassword())) { return redirect:/admin/login?error1; } if (user.getStatus() 0) { return redirect:/admin/login?errorlocked; } session.setAttribute(admin_user, user); session.setAttribute(admin_login_ip, getClientIp(request)); return redirect:/admin/news/list; }逻辑说明BCrypt.checkpw接收的是用户输入的明文密码和数据库里存的哈希值返回布尔值。这里不能先查库再在 Java 代码里做字符串比较更不能用select * from user where username? and password?这种 SQL一旦数据库泄露所有账号密码直接暴露。session 里我只存 admin_user 和 admin_login_ip 两个属性前者用来在页面上展示昵称和角色后者用来记录登录来源后续做安全审计时会用到。参数说明username 和 password 是从表单 POST 过来的表单提交的编码格式要设置成application/x-www-form-urlencoded; charsetUTF-8否则非英文字符的账号名会乱码。如果项目用 Spring Bootserver.servlet.session.timeout默认是 30 分钟新闻后台建议调成 60 分钟运营人员写完一篇长稿回来不用重新登录。3.2 拦截器与统一校验哪些页面必须拦截、哪些不能拦登录只能解决“你进来了”但不能解决“你能进哪里”。新闻系统的权限边界其实就两类一类是后台管理页面必须登录之后才能访问另一类是前台展示页面比如列表页和详情页绝对不能拦否则搜索引擎和直接访问链接的外部读者全都会被重定向到登录页这个翻车场景在真实项目里见得太多了。我一般会在后端写一个 LoginInterceptor 拦截器并把“需要放行的路径”和“需要拦截的路径”分开配置Component public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(false); if (session ! null session.getAttribute(admin_user) ! null) { return true; } // 后台未登录拿到来源路径登录后跳回原页面 String backUrl request.getRequestURI(); response.sendRedirect(/admin/login?back URLEncoder.encode(backUrl, UTF-8)); return false; } }注册拦截器时addPathPatterns只匹配/admin/**同时excludePathPatterns放行/admin/login和/admin/static/**。这个配置被不少人写成拦截所有路径导致前台页面也跳登录——排错的时候会非常困惑。另外注意/admin/login页面自身的静态资源CSS、JS如果被拦截页面会变成无样式状态虽然功能没坏但看起来就是老掉牙的系统答辩时印象分会受影响。3.3 密码存储的三条底线md5 加盐与 BCrypt 选择我接手过一个“新闻管理系统.doc”文档配套的代码密码直接明文存数据库。复制那份代码时把这条也复制了过去结果被主管点名了一回。从那以后密码存储我坚持三条底线一是不用明文二是不用无盐 MD5三是每个用户要有独立的盐。直接用 MD5 的问题在于市面上有大量查表库常见弱口令的哈希值一查一个准。加盐的做法是md5(password salt)但盐值如果固定写在代码里等于没加。更省事的方案是直接用 BCrypt。Spring Security 框架里集成BCryptPasswordEncoder或者单独引入 jBCrypt 依赖代码写作BCrypt.hashpw(password, BCrypt.gensalt())。存库的字符串类似$2a$10$N9qo8uLOickgx2ZMRZoMye...其中包含了盐值和迭代次数校验时BCrypt.checkpw会自动从存储串里提取盐值不需要你额外维护一个 salt 列。这套做法的最大好处是同一个密码每次哈希结果都不一样因为盐值是随机生成的就算数据库被拖走也没法直接反查出原始密码。4. 前台列表页与详情页的渲染浏览量、分页与内容展示的几个必调参数4.1 分页查询LIMIT 和 OFFSET 的边界问题前台列表页的分页新手最容易犯的错是把 pageNum 和 pageSize 直接拼进 SQL然后在LIMIT后面忘了把页码转换成偏移量。正确公式是offset (pageNum - 1) * pageSize。举个例子第一页每页 10 条LIMIT 0,10第二页就是 LIMIT 10,10。如果你直接把 pageNum 拿去当 offset第二页就是从第 1 条开始和第一页完全重复。深度翻页的场景也就是页数到了几百以后OFFSET越大越慢。MySQL 会扫描和丢弃前 N 条记录这个等待在高并发访问时会被放大。我常用的折中方案是限制最大页码做法是查询 count 之后如果 pageNum 超过了总页数直接重定向回第一页。对于新闻管理系统这个体量数据量通常在十万以内LIMIT 加合理索引完全够用没必要引入 Redis 缓存列表页。切记列表页的分页 SQL 要用ORDER BY published_at DESC, id DESC否则同一秒发布的两篇文章会因为时间相同而出现排序跳动。4.2 浏览量计数点击一次加一的并发问题浏览量是新闻管理系统里最典型的一个“看起来简单做起来全是坑”的功能。初级实现是详情页里先SELECT view_count FROM news WHERE id ?然后在 Java 代码里加一再UPDATE news SET view_count ? WHERE id ?。这个操作在低并发下没问题一旦同时有几十个人打开详情页读出来的值全是同一个旧值写回去的时候互相覆盖浏览量基本就卡在最初那个数附近。这个场景面试官特别喜欢追问因为绝大多数简历项目里都有这个 bug。解决方案是把“读-改-写”三步合并成一条原子 UPDATEUPDATE news SET view_count view_count 1 WHERE id #{id};这条语句在 InnoDB 引擎下会给该行加锁多个并发请求会被串行化执行虽然有一定性能损耗但保证了计数绝对准确。之后再发一条不带 view_count 的查询取详情数据。两种方式对比下来少一次查询多一把行锁新闻站的浏览量刷新频率远小于库存扣减频率用这个方案没有任何问题。4.3 详情页的发布时间与来源展示格式化与空值兜底详情页展示发布时间很多人直接news.getPublishedAt().toString()输出一长串带毫秒的时间戳既不好看也不专业。我会在后端用DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss)统一格式化并且对空值做兜底published_at 为 null 时说明这条新闻还在草稿箱里但前台理论上看不到——一旦出现了多半是状态判断漏了。还有一个细节是“来源”字段。很多新闻管理系统的信息来自外部供稿字段里存的是 URL 或媒体名称。前台展示时要做 URL 安全校验否则用户提交的链接可以直接用javascript:alert(1)伪协议点一下就触发脚本。简单的处理方式是只提取http://和https://开头的链接其他一律显示为默认来源这个正则校验虽然简单但能挡掉不少低级攻击。public String safeSource(String rawSource) { if (rawSource null || rawSource.trim().isEmpty()) { return 本站原创; } if (rawSource.startsWith(http://) || rawSource.startsWith(https://)) { return rawSource; } return 本站编辑整理; }这段逻辑放在服务端做而不是前端做是防止有人绕过页面直接拼接口。新闻详情页的读取参数除了分页和浏览量还有一个容易忽略的地方是正文里的图片路径建议存相对路径页面端拼上 CDN 域名或上传域名不要在后端拼死整条 URL否则以后换 CDN 服务商时所有历史新闻都要批量改库。5. 新闻管理系统上线与交付的常见坑乱码、XSS、上传目录5.1 中文乱码从数据库到页面的三层排查后台保存“新时代”三个字再打开页面变成了“鏂版椂浠”这种乱码在新闻系统里太常见了。按我的排查经验先查数据库连接串再查数据库表结构最后查页面编码。现象管理列表页所有中文都显示成问号或者保存成功后刷新变成乱码。原因JDBC 连接串有 90% 的概率没加characterEncodingutf8。如果你的连接串是jdbc:mysql://localhost:3306/news_system那默认字符集跟随服务器设置一旦服务器不是 utf8就全乱了。解决连接串改成jdbc:mysql://localhost:3306/news_system?characterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai同时确认建表时用的DEFAULT CHARACTER SET utf8mb4已经生效——注意 utf8mb4 和 utf8 在连接参数上还是写utf8连接器会自动映射。第三处容易漏的是 Resin/Tomcat 对 GET 请求的 URI 编码Spring Boot 内嵌 Tomcat 默认已是 UTF-8老式独立 Tomcat 需要改server.xml里的URIEncodingUTF-8否则通过${param}传的中文关键词全部乱码。这层检查完通常情况都能解决。5.2 编辑器的 XSS 风险富文本内容为什么不能直接入库后台新闻编辑器用的是富文本用户粘贴的内容可能带上一堆 html 标签。前台展示时如果你直接用th:utext或v-html原样输出等于给访问者发了一张“欢迎执行脚本”的通行证。攻击者在正文里塞进img srcx onerroralert(document.cookie)每个访客打开详情页都会执行这个脚本。现象网上发了一条新闻然后用浏览器打开详情页第一次正常第二次直接弹窗或跳转到广告页。原因内容在服务端保存时没有做 HTML 过滤前端输出时又用了不安全的插值语法。解决服务端做白名单过滤最简单的方案是引入 Jsoup 的Jsoup.clean()只保留 p、br、strong、img、a 等基础标签去掉 script、iframe、onerror、javascript 链接。String safeContent Jsoup.clean(rawContent, Safelist.relaxed() .removeTags(script, iframe, object, embed) .removeAttributes(img, onerror, onload) .addAttributes(img, src, alt, width, height) .addProtocols(a, href, http, https));这段代码里Safelist.relaxed()本身允许的标签比较多需要继续收。addProtocols是限制 a 标签 href 只允许 http 和 https避免javascript:协议。保存进库的已经是过滤后的安全的 HTML前端输出用普通th:text会直接原文显示标签所以这里要用th:utext但前提是入库前过滤这一步绝不能省。安全过滤宁可过严不可过松大不了丢样式不能丢安全。5.3 图片上传的目录权限与文件名冲突上传图片后详情页图片立刻 404是文件上传模块最容易翻车的点。我在本地开发时正常部署到生产环境就挂——原因多半是图片存到了 Tomcat 的部署目录webapps/ROOT/upload下面。而每次重新部署 WAR 包容器会清空或更换工作目录所有上传的文件跟着消失。现象上传成功后前端能看到图片但服务器一重启图片全丢或者换个时间再部署一次图片被新目录覆盖。原因使用 Servlet 容器的真实路径request.getServletContext().getRealPath(/upload)存文件这个路径不稳定。解决在系统配置里指定一个独立于应用的外部目录比如 Linux 下的/data/news_upload然后创建虚拟路径映射让 URL 的/files/**指向磁盘上的绝对目录。Spring Boot 里的映射写法是Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String uploadDir file: System.getProperty(user.home) /news_upload/; registry.addResourceHandler(/files/**).addResourceLocations(uploadDir); } }配置之后上传接口把文件写到/root/news_upload/页面用http://域名/files/2024/covers/xxx.jpg访问和 Tomcat 生命周期完全解耦。文件名一定要用UUID或者“日期 随机数”不能保留用户原始文件名否则一来重名覆盖二来中文文件名在某些浏览器里 encode 不一致会白屏。5.4 定时发布时间到了文章却没出现新闻后台带定时发布功能很常见实现定时任务的代码跑起来了但到了设定时间前台依然看不到文章。排查一圈发现定时任务里查的是status 0的所有新闻把发布时间大于现在或者小于现在的都一起查出来。更隐蔽的问题是只把 status0 改成 status1没有设置 published_at导致列表排序全部错乱。现象设定 18:00 发布的新闻18:05 前台还没出现查看数据库发现 status 已经变成了 1。原因定时任务在 18:00 执行了改状态但前台的列表查询条件是status1 AND published_at now()而任务的发布动作没有同步写 published_at。解决把发布任务改成两步——第一步把待发布status2且 publish_timenow() 的新闻选出第二步在同一事务里更新 status1、published_atnow()。前台查询只认status1 AND published_atnow()这样定时发布和即时发布共用一套展示逻辑不会出现状态和时间的错位。6. 把系统做成“能答辩”的交付物三个值得额外做的小功能6.1 伪静态 URL让详情页更像真实站点答辩和上线时/news/detail?id123这种动态 URL 显得很学生气。我一般建议加一层伪静态规则把详情页地址变成/news/123.html这一步对访问速度没有本质提升但对系统专业度观感和 SEO 都有好处。Spring Boot 里可以写一个PathPatternParser的路由映射或者用过滤器拦截/news/*.html提取中间的数字编号后转发到原有的查询接口。转发的过程中要保持原来的查询参数否则分页信息会丢。6.2 操作日志记录谁在什么时候改了什么新增后台操作日志表记录每次“发布新闻”“删除新闻”“修改分类”动作的管理员 ID、动作类型、目标 ID、IP 地址和时间。实现很简单在拦截器里加一个全局Aspect切面对标注了LogAnnotation的方法做环绕通知。这个功能成本不高但它能让答辩老师看到“审计意识”。真实的新闻发布后台如果出现误删新闻没有日志等于没有后悔药负责的团队都会保留最近 90 天的后台操作行为记录。6.3 数据库备份脚本上线前最后一道防线新闻系统上线后最不想遇到的就是数据库被误清空。我自己的习惯是把备份脚本写进项目文档里让使用的人直接复制就能配置每日备份#!/bin/bash BACKUP_DIR/data/backup/news_system DATE$(date %Y%m%d_%H%M%S) mysqldump -unews_admin -p密码 news_system | gzip $BACKUP_DIR/news_$DATE.sql.gz find $BACKUP_DIR -name news_*.sql.gz -mtime 30 -delete这段脚本先按日期生成带时间戳的备份文件再用 gzip 压缩减少磁盘占用最后通过find -mtime 30清理 30 天前的旧备份。在 crontab 里加一行0 3 * * * /opt/scripts/backup_news.sh每天凌晨三点自动执行。做这个过程时我见过有人直接把密码写在脚本里并提交到版本库这是安全隐患建议把密码改成从受保护的配置文件读取或者用环境变量注入。这一整套做下来数据库、权限、前台、安全、运维基本都照顾到了。我自己每次接这类项目都会保留一套“最小可用 安全加固”的检查清单先保证核心链路能跑通再往上叠加加分项。希望这份拆解能帮你把这个标题做成真正拿得出手的作品祝顺利。本文还有配套的精品资源点击获取