
“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_timecategory资讯分类id, name, sort_order, status, create_timearticle科普文章id, cat_id, title, cover_image, summary, content, author, views, like_count, status, is_top, is_recommend, publish_timeknowledge防控知识库id, disease_name, pathogen, symptoms, prevention, treatment, viewsmessage用户留言id, nickname, contact, content, reply, status, create_time这几张表的设计有一些细节值得说明。article里的status这个字段是个小枚举我用int类型区分0是草稿、1是已发布、2是已下架。用int比用varchar存字符串更省空间查询效率也略高。is_top和is_recommend都设置成tinyint0和1来控制置顶和推荐后台管理列表里用开关切换很方便。对于knowledge表之所以没有设计成资讯分类的形式是因为防控知识库里每一条内容是“病种维度”的完整信息块包括病原体、症状、预防措施、治疗方法用结构化字段存比塞进一篇文章更利于前台做模块化展示也方便用户浏览时按区块查看。数据库字符集和环境保持一致这是很多人忽略的点。建表语句里我统一用utf8mb4不用utf8。原因很简单utf8在MySQL里最多3个字节存不了生僻字和emoji比如用户留言里带了个表情符号直接用utf8的表就会报错或乱码。这个坑在部署阶段遇到就很难受回头改表字符集还有一堆存量数据要迁移。2.3 MyBatis Plus带来的开发效率提升如果纯用MyBatis写XML这个项目的数据访问层代码量会超过一半。用MyBatis Plus之后大部分单表操作的CRUD可以写成一行public interface ArticleMapper extends BaseMapperArticle { }内置的selectById、selectPage、deleteById这些方法直接用就行。分页查询配合PaginationInnerInterceptor插件前台列表接口三行代码就能写出来PageArticle page new Page(current, size); LambdaQueryWrapperArticle 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;charsetUTF-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?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue 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个小时。allowPublicKeyRetrievaltrue这个参数在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_URLhttp://服务器的公网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(ListHttpMessageConverter? converters) { converters.stream() .filter(c - c instanceof StringHttpMessageConverter) .forEach(c - ((StringHttpMessageConverter) c).setDefaultCharset(StandardCharsets.UTF_8)); } }MySQL乱码在上面已经提到统一库表字符集为utf8mb4、连接串加characterEncodingutf8mb4就能解决。排查乱码问题时建议先确认“哪个环节出的乱码”——浏览器渲染前接口返回就乱是后端编码问题接口返回正常但数据库里存的是乱码是连接串问题。别急着改代码先定位环节。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里把“为什么这么设计”讲明白。做到这个程度这套系统的设计与实现就算真正过关了。