“weixin063传染病防控宣传系统的设计与实现”,光看这个标题,就能猜出这是又一道经典的毕业设计选题。说实话,这类“XX宣传系统”“XX管理系统”在每年的高校毕设里出现的频率极高,表面看是重复度很高的题目,但真正动手做起来,里面的门道比想象中多得多。我最近完整跟完了一个类似的防控宣传系统项目,从需求梳理、技术选型到编码实现、部署上线,过程里踩了不少坑,也沉淀了一些很实用的套路。这篇文章就把整个项目从0到1的完整过程拆开讲清楚,尤其是那些你照着教程做也会忽略的细节,适合正在做毕设、或者刚入门做Web项目的同学参考。
整个系统的本质是一套“内容管理系统(CMS)”,核心场景是健康科普与防控宣传:管理员在后台发布传染病科普文章、输出防控公告、管理知识库,普通用户在前台浏览资讯、按分类检索内容、查询防控知识、提交在线留言。搞清楚这个本质,很多设计决策就会变得很清晰——它不是企业级中台,也不是高并发平台,而是一个“信息展示 + 内容管理 + 基础交互”三位一体的典型Web应用。
1. 项目背景与需求拆解
1.1 这类“宣传系统”到底在解决什么问题
学校、社区、医院、企业这类机构,经常需要做健康教育宣传。传统的做法是贴海报、发传单、开讲座,覆盖面和留存效果都有限。做一个线上宣传系统,最大的价值在于三点:一是信息可沉淀,科普文章发出去之后长期可查;二是覆盖面广,用户用手机就能访问,不需要专门跑一趟;三是可统计,管理员能清楚看到每篇文章被看了多少次、用户更关心哪类内容。
拿这次的项目来说,需求方是一家基层社区卫生机构,他们原来连个像样的科普发布渠道都没有,健康宣传活动全靠线下。这个系统上线后,管理员在后台编辑一篇传染病科普图文,前台马上就展示出来,居民扫码就能看,还能在文章下面留言提问。这种“一次发布、多人阅读、随时更新”的模式,才是宣传系统最核心的日常价值。
1.2 从标题反推功能清单
拿到“传染病防控宣传系统”这个题目,不要急着写代码。先做需求拆解,把系统分成“前台展示”和“后台管理”两个大的端来看。
前台门户端要有的核心功能:
- 首页:轮播公告、推荐资讯、分类导航、最新科普文章列表
- 资讯列表与详情:按分类筛选文章,文章详情支持图文混排
- 防控知识库:按病种分类整理的防护手册,支持关键词查询
- 留言互动:访客提交咨询与反馈,展示已审核通过的留言
后台管理端要有的核心功能:
- 管理员登录与权限校验
- 资讯管理:文章的增删改查、置顶、推荐、发布与下架
- 分类管理:科普分类的增删改,分类排序
- 留言管理:审核、回复、隐藏违规留言
- 数据统计:文章浏览量统计、分类文章数量统计
把这8个功能点列出来之后,整个系统的范围就清晰了。功能不追求多,但每个模块都要完整闭环。很多同学毕设中途卡住,就是因为一开始没拆需求,上来就建表,结果写着写着发现字段对不上、页面缺板块,反复返工。
1.3 技术选型的核心权衡
这个题目下最稳妥的组合是:后端Spring Boot + MyBatis Plus,前端Vue + Element UI,数据库MySQL。之所以这么选,一是这个组合的资料极其丰富,几乎每个报错都能搜到解决方案;二是Spring Boot的约定优于配置,省掉了大量XML配置工作,适合一个人完成全栈开发。
如果前端的JavaScript基础比较薄弱,也可以用服务端渲染的模板方案——Spring Boot + Thymeleaf + Bootstrap,甚至直接用JSP。它的好处是后端直接返回完整页面,不用处理跨域、不用写前后端联调接口文档,开发链路更短。我当时给这个项目配的方案是前后端分离,因为那位开发者想顺便练一下Vue,同时后续系统要接入更多的页面交互,分离架构扩展起来更舒服。
2. 系统架构与数据库设计
2.1 整体架构分层
系统采用经典的三层架构,再加上前端工程,分工是这样的:
- 前端展示层:Vue页面组件,负责数据渲染、路由跳转、用户交互,通过HTTP接口请求后端数据
- 控制层(Controller):接收前端请求,做参数校验,调用服务层处理业务
- 业务层(Service):实现核心业务逻辑,比如资讯发布的参数组装、留言审核的状态流转
- 数据访问层(Mapper):基于MyBatis Plus操作MySQL表,避免手写大量SQL
- 基础设施层:MySQL存储数据,Redis做简单缓存(可选,本地环境可以不用)
同时还有两个横切关注点:全局异常处理,统一把业务异常转换为友好的JSON提示;登录拦截器,对后台管理的所有接口做会话校验,未登录请求直接返回401。
2.2 数据库表结构设计
数据库是这类系统的核心,表不在多,而在设计合理。这个项目一共设计了5张核心表,完全够用。
| 表名 | 用途 | 关键字段 |
|---|---|---|
| admin_user | 管理员账号 | id, username, password, real_name, avatar, role, status, create_time |
| category | 资讯分类 | id, name, sort_order, status, create_time |
| article | 科普文章 | id, cat_id, title, cover_image, summary, content, author, views, like_count, status, is_top, is_recommend, publish_time |
| knowledge | 防控知识库 | id, disease_name, pathogen, symptoms, prevention, treatment, views |
| message | 用户留言 | id, nickname, contact, content, reply, status, create_time |
这几张表的设计有一些细节值得说明。article里的status这个字段是个小枚举,我用int类型区分:0是草稿、1是已发布、2是已下架。用int比用varchar存字符串更省空间,查询效率也略高。is_top和is_recommend都设置成tinyint,0和1来控制置顶和推荐,后台管理列表里用开关切换很方便。
对于knowledge表,之所以没有设计成资讯分类的形式,是因为防控知识库里每一条内容是“病种维度”的完整信息块,包括病原体、症状、预防措施、治疗方法,用结构化字段存比塞进一篇文章更利于前台做模块化展示,也方便用户浏览时按区块查看。
数据库字符集和环境保持一致,这是很多人忽略的点。建表语句里我统一用utf8mb4,不用utf8。原因很简单:utf8在MySQL里最多3个字节,存不了生僻字和emoji,比如用户留言里带了个表情符号,直接用utf8的表就会报错或乱码。这个坑在部署阶段遇到就很难受,回头改表字符集还有一堆存量数据要迁移。
2.3 MyBatis Plus带来的开发效率提升
如果纯用MyBatis写XML,这个项目的数据访问层代码量会超过一半。用MyBatis Plus之后,大部分单表操作的CRUD可以写成一行:
public interface ArticleMapper extends BaseMapper<Article> { }内置的selectById、selectPage、deleteById这些方法直接用就行。分页查询配合PaginationInnerInterceptor插件,前台列表接口三行代码就能写出来:
Page<Article> page = new Page<>(current, size); LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Article::getStatus, 1) .eq(Article::getCatId, categoryId) .orderByDesc(Article::getPublishTime); articleMapper.selectPage(page, wrapper);LambdaQueryWrapper用起来很顺手,不会因为字段改名导致运行时错误。逻辑删除这件事也要说一句:后台资讯删除操作,我建议不要物理删除,用@TableLogic注解加一个deleted字段,这样误删了还能恢复,也保留完整的数据记录。毕设答辩时老师问起“如何避免误操作”,这能成为一个加分回答。
3. 前端页面与核心交互实现
3.1 前台页面布局
前台门户走的是简洁路线,只要信息结构清楚、层级明确就行。设计时我把页面分成四个核心区域:
- 顶部导航:系统名称、资讯分类菜单、知识库入口、留言板入口
- 首页主体:轮播图(展示重要公告)、最新资讯推荐列表、热门科普排行
- 列表页面:按分类展示资讯的标题、摘要、发布时间和浏览量
- 详情页面:文章完整内容展示,底部是留言区
布局框架上,我直接用Vue Router管理页面路由,公共的头尾部分抽成组件。首页数据通过接口一次性拉取:查轮播图、查置顶文章、查最新资讯列表,三个请求并行发送,前端用Promise.all做合并渲染。这里的并行请求优化值得注意——分三个接口的好处是数据各归各管,其中一个请求失败不影响其他区域展示。
3.2 后台管理的设计逻辑
后台管理页面的核心不是花哨的UI,而是“高效操作”。布局采用左侧菜单栏+右侧内容区。左侧菜单分四大项:仪表盘、资讯管理、分类管理、留言管理。
资讯管理页面里,列表字段展示标题、分类、浏览量、发布状态、置顶状态和操作按钮。
操作按钮里有一个容易忽略的细节:已经发布的文章,操作列显示的是“下架”而不是“删除”;草稿状态的文章,显示的是“编辑”“发布”“删除”。这个状态感知设计能让管理员明确知道当前这条数据能做什么,避免误操作。发布文章时表单里有一个“发布”按钮和一个“存入草稿”按钮,点击发布前前端会做必填校验,标题、分类、内容三样缺一不可。
后台接口的安全校验用拦截器实现:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); Object adminInfo = session.getAttribute("adminInfo"); if (adminInfo == null) { response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"未登录或登录已过期\"}"); return false; } return true; } }注册时注意排除登录接口和静态资源路径,避免拦截器把登录请求本身也拦了。
3.3 富文本编辑与图片上传的处理
后台发布资讯一定会用到富文本编辑器。这个项目用的是wangEditor,轻量、接入快,而且对中文文档友好。
富文本编辑有一个“图片怎么存”的经典问题。默认情况下,粘贴图片到编辑器会以base64字符串的形式嵌进HTML内容,一张几MB的图转成base64后直接膨胀三分之一到一倍。如果文章里有20张图,那一条记录的content字段就可能几十MB,数据库压力极大,页面加载也会卡死。
解决方案是:图片直接走图片上传接口,上传成功后返回图片URL,编辑器把URL插到content里。前端配置上传逻辑:
editorConfig.MENU_CONF = { uploadImage: { server: '/api/admin/upload/image', fieldName: 'file', maxFileSize: 5 * 1024 * 1024, onSuccess(file, res) { return res.data.url; } } };后端上传接口接收MultipartFile后,把文件保存到服务器上配置好的本地上传目录,或者直接传到对象存储,返回一个可访问的URL。保存文件时文件名不要用原始名称,用UUID或者日期+随机数重新生成,避免重名覆盖和安全风险。
4. 核心功能模块实现要点
4.1 资讯发布与展示的完整流程
资讯模块是整个系统的主动脉。我把它分成两条链路来讲清楚。
发布链路(后台):
- 管理员填写表单,前端收集标题、分类、摘要、封面、富文本内容等数据
- 提交到后端的/api/admin/article/save接口
- 控制层接收参数后做基础校验,比如标题不能为空、长度不超过100字符
- 服务层组装Article实体,补充作者ID、浏览量清零、状态草稿/发布
- 调用MyBatis Plus的insert方法完成入库
这里有几个容易踩坑的地方。第一个是cover_image封面图,如果管理员没有上传封面,前端应该给一个默认的占位图URL,保证列表页面布局不会因为没有封面而塌掉。第二个是publish_time处理,我只在状态切换为“发布”时才写入当前时间,而不是每次编辑都覆盖发布时间,否则一篇文章改个错别字,前台就会显示成刚刚发布,干扰用户判断。
展示链路(前台):
- 文章列表页调用分页查询接口,获取当前分类下的文章列表
- 详情页根据articleId查询文章,注意这里要处理浏览量。我在详情接口里做了自增:
UPDATE article SET views = views + 1 WHERE id = ?,这样每打开一次详情就加一次浏览,不依赖前端上报,简单可靠。 - 文章内容返回给前端用v-html渲染富文本内容
v-html渲染有个安全细节:富文本内容里如果嵌入了用户可写的脚本代码,即使管理员才能发文章,也建议在服务端做一次HTML标签白名单过滤,保留p、h2、h3、img、ul、ol等常用标签,过滤script、iframe这类危险标签,防止XSS注入。
4.2 知识库检索与命中高亮
知识库模块在前台需要支持搜索。这个搜索做了很轻量的实现:用户输入关键词,后端用MySQL的LIKE查询匹配disease_name和prevention字段,把结果按浏览量排序。
SELECT * FROM knowledge WHERE disease_name LIKE CONCAT('%', #{keyword}, '%') OR prevention LIKE CONCAT('%', #{keyword}, '%') ORDER BY views DESC这种模糊查询在数据量几百条的场景下完全够用,不建议一上来就上Elasticsearch或者全文索引,那是过度设计。我做了一层小的优化:查询结果返回给前端后,前端会用关键词做一次命中高亮,把科普文章里出现关键词的部分用背景色标出来,用户一眼就能看到自己搜的东西在文中的哪里,体验提升明显。
4.3 留言板审核与防刷
留言板是前台弱势群体,也是最容易被攻击和灌水的地方。我做了一套“提交在前台,审核在后台,展示需通过”的流程。
前台提交留言时,只允许填昵称、联系方式、留言内容三个字段,后端校验内容长度在10到500字之间,联系方式做简单正则校验。提交成功后直接提示“留言已收到,审核通过后将在页面展示”,这个用户体验是诚实的,因为审核流就是这么设计的。
后台留言管理列表里,每条留言有通过、隐藏、删除三个操作。通过之后前台可见,隐藏则从页面撤下,并不删除,数据先留着。为了防止机器人灌水,留言接口加了一个简单的频率限制:同一个IP在五分钟内只能提交一次。用Redis实现也就是一条INCR加过期时间的操作:
String key = "message:ip:" + ip; Long count = redisTemplate.opsForValue().increment(key); if (count != null && count > 1) { return Result.error("操作过于频繁,请稍后再试"); } redisTemplate.expire(key, 5, TimeUnit.MINUTES);如果没有引入Redis,也可以在本地定义一个ConcurrentHashMap存IP和最近提交时间,单机部署也够用,但系统重启后记录会丢失。因此正式部署我还是建议把Redis加上,代码成本不高。
5. 部署配置与上线流程
5.1 数据库初始化与环境配置
部署的第一步是准备好生产数据库。我用Navicat导出SQL脚本,再在服务器的MySQL里执行。这里就出现了一个环境差异问题:本地MySQL版本是8.0,服务器上是5.7,建表语句里的某些索引类型和字符集默认值不一致,会导致导入报错。解决办法是导出时选择兼容模式,或者在服务器上把数据库版本也统一成8.0。
application.yml里的关键配置需要针对生产环境调整:
spring: datasource: url: jdbc:mysql://localhost:3306/prevention_system?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 实际的强密码 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 10MB max-request-size: 20MB这里的serverTimezone必须显式设置成Asia/Shanghai,否则很多服务器默认的时区是UTC,数据库存进去的时间和实际时间相差8个小时。allowPublicKeyRetrieval=true这个参数在MySQL 8.0的客户端连接里必须要加,不然会报Public Key Retrieval is not allowed的错。
5.2 后端打包与前端构建
后端打包用Maven的package命令,生成jar包。打包之前有个容易忘的事:确认test代码不会在打包阶段执行失败。日常开发中写的测试类可能依赖本地环境,服务器上一跑就挂了。我统一在pom.xml里配置了maven-surefire-plugin跳过测试。
前端构建是另一个容易出问题的地方。Vue项目默认开发环境的接口地址是localhost:8080,构建生产包之前必须先改配置文件,把API请求的baseURL换成服务器IP或者域名:
// .env.production VUE_APP_BASE_URL=http://服务器的公网IP:8080/api改完后执行npm run build,生成dist目录。把这个目录里的静态文件放到Nginx的html目录下,同时配置Nginx把/api路径反向代理到Spring Boot应用的8080端口:
server { listen 80; server_name 服务器IP; 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这行很关键,因为Vue是单页应用,路由由前端Router控制,如果用户直接访问某个子路径的URL(比如/refresh),刷新页面时Nginx找不到对应的物理文件,必须fallback到index.html,让前端路由接管。漏掉这一行,部署完就会出现“首页能打开,子页面一刷新就404”的经典问题。
5.3 进程守护与日志查看
后台服务直接用java -jar启动,窗口一关服务就没了。生产环境建议用systemd把Spring Boot应用注册成系统服务,设置开机自启,崩溃自动重启。systemd配置里还可以把标准输出和错误输出重定向到日志文件,排查问题时直接tail -f 日志文件,效率会高很多。
6. 常见问题与排查技巧实录
6.1 前台页面中文乱码
乱码问题在我这次排障过程中出现过两次,一次是HTTP接口返回JSON里的中文乱码,另一次是MySQL存储的中文乱码。
HTTP接口乱码的根源是响应头的字符编码不是UTF-8。Spring Boot在配置类里加一个通用的消息转换器配置即可:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { converters.stream() .filter(c -> c instanceof StringHttpMessageConverter) .forEach(c -> ((StringHttpMessageConverter) c).setDefaultCharset(StandardCharsets.UTF_8)); } }MySQL乱码在上面已经提到,统一库表字符集为utf8mb4、连接串加characterEncoding=utf8mb4就能解决。排查乱码问题时建议先确认“哪个环节出的乱码”——浏览器渲染前接口返回就乱,是后端编码问题;接口返回正常但数据库里存的是乱码,是连接串问题。别急着改代码,先定位环节。
6.2 富文本里的图片上传失败
这个坑的现场是这样的:编辑器里文字能正常输入,但一点图片上传就报403,一看Nginx错误日志,提示request body size too large。
原因很直接:Nginx默认client_max_body_size是1MB,图片稍大就被拦下了。在Nginx配置的server块里加上client_max_body_size,同时Spring Boot侧的文件大小限制也要同步调大,两边都要改才能生效。改完之后重启Nginx,问题立刻解决。
6.3 后台接口一直提示未登录
前后端分离模式下,登录状态依赖Cookie,而Vue前端开发时用localhost:8080,后端接口在localhost:8081,这是两个不同的源,Cookie在跨域请求中默认不会携带。
解决方式有两种。第一种是开发环境用Vue的devServer做代理,把/api的请求转发到后端,让浏览器认为所有请求都在同一个源下,Cookie可以正常工作:
// vue.config.js module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } };第二种方式是用后端返回token替代session,前端每次请求在请求头里携带token。考虑到这个系统就是单机部署、用户量不大,用Session机制最省事,同时配合代理解决跨域。如果换成token方案,后端要写一套拦截逻辑从请求头取token,工作量会多一些,没必要为了“技术新”去上这个复杂度。
6.4 浏览量统计不准确
上线第二天我看浏览量数据,发现每隔几分钟刷一次页面,浏览量就加很多。原因是详情页接口被我写成只要查看就自增,搜索引擎的爬虫抓取页面也会触发这个操作。
改进办法很简单:自增逻辑只对真实用户开放,前端进入详情页时把当前状态记录在本地存储里,同一篇文章在浏览器里一天只记一次浏览。服务端接口侧也可以加一个简单的请求来源判断,过滤掉明显的爬虫User-Agent,让数据更干净。
7. 写在项目收尾之后
整个项目做完,最大的体会是:这种宣传管理类的系统,难度不在某一个单独的技术点上,而在对业务细节的通盘考虑。从需求拆解到数据库设计,从资讯发布链路到留言审核流,再到部署上线的环境问题,每一步都有值得琢磨和实践的地方。
项目中大量用到了Spring Boot、MyBatis Plus、Vue、MySQL这套技术组合。对于正在做类似题目的同学,我的建议是先不要陷入代码细节,把需求文档和数据库设计搞明白再动手。数据库字段的类型、状态设计、表之间的关联,这些东西一开始没想清楚,后面写代码的时候会被反复打断,耗时翻倍。
最后分享一个部署排查的小技巧:项目上线后如果出现“本地没问题但线上就是不行”的情况,不要怀疑代码差异,90%的问题是环境配置不同。按“数据库连接—字符编码—文件路径—Nginx转发—服务器端口”这个顺序逐层排查,比乱改业务代码高效得多。这套排查思路,我在这个宣传系统项目里至少救了两次“命”。
想把这个题目做成拿得出手的作品,就把每一个模块的边界做清晰,接口返回结构统一,异常处理完备,再在答辩PPT里把“为什么这么设计”讲明白。做到这个程度,这套系统的设计与实现就算真正过关了。