1. 选题价值拆解为什么博物馆展览门户系统是毕业设计的安全牌与加分牌每年到毕业设计选题季总有一批同学在做功能新颖但可能烂尾的题目和做经典老套但稳定能过审的题目之间反复纠结。我个人的建议一直是如果你不是手里已经握着一个企业级真实项目或者有足够强的自学能力去驾驭一个完全陌生的领域那就老老实实选一个你熟悉技术栈上的传统题目。博物馆展览门户系统就是这个范畴里性价比非常高的一类。先说这个题目的实际价值。博物馆、美术馆、科技馆这类文化场馆的线上门户本质上是一个内容展示 信息管理 轻量互动的三位一体系统。它不像电商那样需要复杂的订单状态机和支付对账也不像社交平台那样需要高并发消息推送但它该有的东西一样不少前台要有多级页面展示、搜索筛选、详情页、预约表单后台要有登录认证、权限控制、内容发布、数据统计。这套东西覆盖了SpringBoot和Vue3最核心的日常开发场景做完之后你对前后端分离项目到底怎么协作这件事会有一个完整的体感。再看这个选题的答辩优势。博物馆系统天然自带文化数字化公共文化服务这些主流价值词汇开题报告和论文的研究意义部分非常好写不需要硬凹一些虚假的应用场景。而且它的用户角色清晰——游客、注册用户、管理员三种角色的权限边界天然分明这在设计数据库和做接口鉴权时非常顺手。还有一个很现实的理由这类系统的源码和参考资料足够多但正是因为它看起来简单很多人的实现都停留在能跑就行的层面。如果你能在这个基础上做出几个有亮点的模块比如展品3D预览的接入思路、预约时段的并发控制、后台的按需发布立刻就能在一堆雷同题目里拉开差距。这篇博文我就以我最近整理的一套SpringBoot Vue3博物馆展览门户系统为例完整拆解它的设计思路、核心实现、踩坑记录和源码使用方法给正在纠结选题或者已经开写但卡壳的同学一个可以直接参考的路线。2. 技术选型的真实理由不是单纯因为新而是因为有据可查、有坑可避2.1 后端为什么用SpringBoot而不是SSH或纯Servlet很多教材还在讲SSHStruts2 Spring Hibernate但说句实在话这套组合在实际开发里已经基本退出历史舞台了。SpringBoot最大的价值在于约定优于配置——你不需要再写一堆XML配置文件来声明每一个Bean、每一处事务边界只需要引入对应的starter依赖框架会自动完成大部分装配工作。以博物馆系统为例我们需要的核心后端能力无非是这几个Web接口层接收前端请求返回JSON数据数据持久化操作MySQL中的展品、展览、预约记录权限控制区分游客、用户、管理员三种角色文件存储展览海报、展品图片的上传与访问SpringBoot对应这四件事的解决方案都非常成熟SpringMVC处理接口Spring Data JPA或MyBatis处理持久化Spring Security或Sa-Token处理权限本地存储或OSS处理文件。每一块都有海量文档和问答遇到问题随手一搜就有答案。对于毕业设计这种时间有限但必须自己独立完成的场景生态的成熟度比技术本身的新旧重要得多。2.2 前端为什么选Vue3而不是Vue2或ReactVue3在2020年正式发布到现在已经过了好几个版本迭代Composition API组合式API语法已经非常稳定。我推荐Vue3的原因很直接第一Vue3 Element Plus这套组合是目前国内后台管理系统的事实标准。博物馆门户虽然偏展示型但后台管理界面占了整个系统的一半工作量用Element Plus可以大幅降低你写表格、表单、弹窗、分页这些通用组件的时间。第二Vue3的script setup语法让组件逻辑的书写非常直观。用Vue2的Options API时数据、计算属性、方法、侦听器分散在多个选项中一个功能的相关代码被拆得七零八落而script setup中你可以把一段业务逻辑完整地写在一起阅读和调试的体验都好很多。第三从答辩和找工作的角度说Vue3是当前市场需求的主流方向做完这个项目你写进简历里的技术栈是Vue3 Vite Pinia而不是已经处于维护尾声的Vue2。同样的工作量简历含金量要高一个档次。2.3 数据存储与鉴权方案的落地搭配数据存储这块MySQL依然是毕业设计最稳妥的选择。关系型数据库适合博物馆这种数据间关联关系明确的场景一个展览包含多个展品一个展品挂在某个分类下一条预约记录关联一个用户和一个展览这些用外键和连表查询就能清晰表达。如果你的开发机内存不大也可以直接用H2数据库先跑通逻辑最后再切MySQL不过我不建议这么做——MySQL的安装和配置本身也是你答辩时要面对的问题之一早点用早点踩坑。鉴权方案我推荐用Sa-Token而不是Spring Security。原因可能有同学不太认同但我还是要说Spring Security的功能确实强大但它的过滤器链机制和复杂的配置项对初学者极不友好光是一个如何放行登录接口、拦截受保护接口的问题就能卡住大多数人两三天。Sa-Token的API简单直接登录后调用StpUtil.login(userId)校验时调用StpUtil.checkLogin()几行代码就能完成会话管理和权限控制对毕业设计这种规模的项目完全够用。提示如果你的导师特别强调必须使用Spring Security那还是以导师要求为准。但如果是自由选型Sa-Token能把你在权限上花的时间从三天压缩到三小时省下来的时间足够你打磨好几个功能细节。3. 系统整体架构与数据库设计先把骨架搭对后面才不会散架3.1 前后端分离的目录结构与请求链路我见过不少毕业设计代码前后端代码乱成一锅粥Vue组件里直接写axios请求地址Controller里返回视图名称甚至有人把druid监控页面和Swagger搞混淆。前端分离项目的第一步就是把目录职责划分清楚。我建议的工程结构是museum-portal/ # 项目根目录 ├── backend/ # SpringBoot后端 │ ├── src/main/java/com/museum │ │ ├── controller/ # 接口层只接收参数、调用服务、返回结果 │ │ ├── service/ # 业务层业务逻辑、事务控制 │ │ ├── mapper/ (或 repository/)# 数据访问层 │ │ ├── entity/ # 实体类 │ │ ├── dto/ # 数据传输对象与实体解耦 │ │ ├── config/ # 配置类跨域、拦截器、文件上传等 │ │ └── common/ # 统一返回结果、异常处理、工具类 │ └── src/main/resources │ ├── application.yml │ └── mapper/ # MyBatis XML文件如果用MyBatis └── frontend/ # Vue3前端 ├── src/ │ ├── api/ # 接口请求封装 │ ├── views/ # 页面组件 │ ├── components/ # 公共组件 │ ├── router/ # 路由配置 │ ├── store/ # Pinia状态管理 │ └── utils/ # 请求工具、工具函数 └── vite.config.js请求链路只有一条主线Vue页面中的方法调用api/xxx.js里封装的请求函数请求函数通过axios发送HTTP请求到SpringBoot的ControllerController调用Service处理业务逻辑Service通过Mapper操作数据库结果逐层返回最终由Vue页面渲染。这条链路里每一个环节都值得你多问一句为什么。比如Vue层为什么要再包一层api文件而不是直接在页面里写axios因为当多个页面都需要调同一个接口时你只需要改一处封装而且接口地址集中在api目录里和后台路由一一对应维护成本低得多。Service层为什么不能直接在Controller里写SQL因为业务逻辑和接口协议是两个维度的职责混在一起后一旦预约规则调整比如新增一个每人每天最多预约两场的限制你就要在Controller里翻找逻辑改完还要担心影响接口返回格式。3.2 数据库表设计六张核心表的字段与关联关系数据库设计是毕业设计答辩时老师必问的环节。博物馆门户系统的表结构不复杂但每一张表的字段都要能自圆其说。我这套系统一共设计了六张核心表表名用途关键字段user用户表id, username, password, nickname, avatar, phone, role, create_timecategory分类表常设展/临时展等id, name, sort, descriptionexhibition展览表id, category_id, title, cover_url, intro, content,status, start_time, end_time, view_countexhibit展品表id, exhibition_id, name, image_url, description, era, materialreservation预约表id, user_id, exhibition_id, visit_date, visit_time_slot, status, create_timecarousel轮播图表首页展示id, image_url, link_url, sort, status多看两眼这几张表之间的关系你会发现它其实是一个典型的主表-从表结构exhibition是核心业务表category给展览做分类exhibit挂在展览下面形成一对多reservation则是用户和展览之间的多对多关联。carousel跟其他表没有外键关系纯粹是前台首页的配置项。这里有几个字段设计的细节要特别说。第一exhibition表里的status字段。我建议用0表示草稿1表示已发布2表示已下线。这个字段是后台发布管理功能的灵魂——管理员添加一个展览后先存草稿审核确认无误后再点击发布前台才能看到。很多同学的实现是添加完直接展示完全不考虑内容审核的环节这在你自己的系统里当然能跑但放到答辩场景下老师一句真实业务中发布内容不需要审核吗就能问住你。加一个状态字段和对应的接口逻辑成本很低但体现的是对业务的理解。第二密码字段。数据库里绝对不要存明文密码用BCrypt加密。SpringSecurity的BCryptPasswordEncoder或者Sa-Token配套的加密工具都行。存储格式类似$2a$10$7QvBOYF...这是一种自带随机盐的哈希结果即使两个用户密码相同数据库中存储的字符串也不一样能有效对抗彩虹表攻击。第三所有带时间含义的字段尽量使用datetime类型。有些同学图省事用varchar存时间字符串做日期范围查询时就会遇到各种格式转换的麻烦BETWEEN比较的结果也不可靠。4. 核心功能模块的实现拆解从能跑到经得起问4.1 展览展示模块首页聚合查询与浏览量防刷设计前台首页是游客对这个系统的第一印象也是最容易做出视觉亮点的地方。我的实现思路是首页由轮播图、热门展览、最新上线、展览分类导航四个区域组成对应后端一个聚合接口GET /api/home一次性返回所有数据避免前端发多个请求。这里有个值得展开讲讲的设计点热门展览的排序规则。最笨的写法是直接按view_count降序排列但这会导致一个问题——老展览浏览量越来越大新展览永远没有机会上榜。我在实际实现时用的是时间衰减策略SQL大致是这样SELECT id, title, cover_url, view_count, (view_count / (TIMESTAMPDIFF(DAY, create_time, NOW()) 2)) AS hot_score FROM exhibition WHERE status 1 ORDER BY hot_score DESC LIMIT 6;这个hot_score的本质是日均浏览量分母加2是为了防止刚发布的展览因为发布时间太短导致分数虚高。这种计算方式在答辩时很有说头它不仅展示了SQL编写能力还体现了一种朴素的推荐算法思想。关于浏览量统计还有一个不得不防的问题刷量。如果每一次前端请求都直接UPDATE exhibition SET view_count view_count 1那么一个攻击者写个for循环就能把某条展览刷到几百万浏览量。我的做法是加一层简单的防刷逻辑在后端用一个HashMap记录每个用户或IP最近一次访问展览的时间如果两次访问间隔小于30秒就不增加浏览量。private MapInteger, Long visitorRecord new ConcurrentHashMap(); public void increaseViewCount(Integer exhibitionId, String visitorKey) { long now System.currentTimeMillis(); Long lastVisit visitorRecord.get(visitorKey); if (lastVisit null || now - lastVisit 30_000) { exhibitionMapper.increaseViewCount(exhibitionId); visitorRecord.put(visitorKey, now); } }这个方案当然不是完美的ConcurrentHashMap存内存重启后就清空了但对于一个毕业设计级别的系统来说完全够用。真正能体现你水平的是你意识到有刷量这个问题并且采取了措施这就是从能跑走向经得起问的关键。4.2 展品详情与搜索筛选多条件组合查询的实现思路展品列表页是一个典型的多条件筛选场景。我们来看几个筛选维度所属展览、时代、材质、搜索关键词、分页。如果为每一种组合都写一个单独的SQL那Mapper里的方法数量会爆炸如果全部拼在一个SQL里动态判断又容易出现SQL注入问题。我采用的方式是使用MyBatis的动态SQL标签本质上是让MyBatis的XML帮你做安全性可控的SQL拼接select idsearchExhibits resultTypecom.museum.entity.Exhibit SELECT * FROM exhibit where if testkeyword ! null and keyword ! AND (name LIKE CONCAT(%, #{keyword}, %) OR description LIKE CONCAT(%, #{keyword}, %)) /if if testexhibitionId ! null AND exhibition_id #{exhibitionId} /if if testera ! null and era ! AND era #{era} /if if testmaterial ! null and material ! AND material #{material} /if /where ORDER BY id DESC LIMIT #{offset}, #{pageSize} /selectwhere标签会自动处理第一个条件前不加AND的问题这是MyBatis很实用的一个特性。所有参数都用#{}预编译方式传递从机制上规避了SQL注入风险。在接口设计上我建议把筛选参数统一封装到一个QueryDTO里Controller的方法参数接收这个DTOService层直接把它传给MapperGetMapping(/exhibits) public Result searchExhibits(ExhibitQueryDTO query) { // query中封装了 keyword, exhibitionId, era, material, pageNum, pageSize return Result.success(exhibitService.searchExhibits(query)); }这样做的好处是以后你要加一个筛选维度比如出土地点只需要在DTO里加一个字段、在XML里加一个ifController和Service的代码一行都不用动。4.3 预约流程模块并发场景下如何防止超约预约是博物馆系统里最有技术含量的功能没有之一。表面看起来就是一个表单提交但仔细想一个问题某展览某天上午时间段只剩最后一个名额两个用户同时点击预约系统应该怎么处理最简单的写法是public Result createReservation(ReservationDTO dto) { int count reservationMapper.countByTimeSlot(dto.getExhibitionId(), dto.getVisitDate(), dto.getTimeSlot()); if (count MAX_CAPACITY) { return Result.error(该时段已约满); } reservationMapper.insert(dto); return Result.success(); }这个写法在并发量低的时候看不出问题但在两个请求同时执行到count查询那一刻两个请求查到的最新数量可能都是MAX_CAPACITY - 1于是两个都通过了if判断最终数据库里就多了两条预约记录超过容量限制。这就是经典的超卖问题。对于毕业设计我推荐一个足够简单但绝对有效的方案在reservation表上建立一个唯一约束把(user_id, exhibition_id, visit_date, visit_time_slot)四个字段组合起来建一个唯一索引让数据库从机制上保证同一个用户在同一天同一时段只能预约同一展览一次。另外一个实用方案是利用MySQL的SELECT ... FOR UPDATE悲观锁将同一时间段的预约记录锁住强制串行化Transactional public Result createReservation(ReservationDTO dto) { // 锁定该时段的所有预约记录其他请求必须等待 ListReservation locked reservationMapper.lockByTimeSlot( dto.getExhibitionId(), dto.getVisitDate(), dto.getTimeSlot()); if (locked.size() MAX_CAPACITY) { return Result.error(该时段已约满); } reservationMapper.insert(dto); return Result.success(); }两个方案里唯一索引是兜底方案悲观锁是业务逻辑层的控制方案。两者配合使用既能在业务层面给用户友好提示又能防止极端情况下的数据错乱。这个唯一索引兜底 逻辑层控制的双保险思路本身就是答辩时可以展开讲三分钟的亮点。4.4 后台管理模块JWT登录鉴权与拦截器配置后台管理系统的核心是权限控制。我用Sa-Token来实现登录和鉴权流程如下用户提交账号密码后端校验通过后调用StpUtil.login(userId)Sa-Token会自动生成一个token字符串返回给前端前端把它存储在本地我建议存到localStorage刷新页面不会丢失之后每次请求在axios拦截器中把token塞到请求头里后端在WebMvcConfig中注册一个Sa-Token拦截器通过SaInterceptor统一校验所有非放行接口。Configuration public class SaTokenConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor(handle - StpUtil.checkLogin())) .addPathPatterns(/api/**) .excludePathPatterns( /api/auth/login, /api/home, /api/exhibitions/**, /api/exhibits/**, /api/carousel/** ); } }对管理员接口的权限控制可以在Controller方法上加SaCheckRole(admin)这样即使普通用户登录了调用管理接口时也会被拦截。注意不要把token简单地存到浏览器的localStorage里就万事大吉。真正上线时需要考虑XSS攻击——攻击者通过注入脚本读取你localStorage里的token。虽然毕业设计答辩不会有人故意攻击你的系统但你至少应该知道这个风险。稍微正规一点的思路是接口除登录外都校验token前端路由守卫根据登录状态决定是否放行页面这两条配合使用。5. 开发与部署中必须跨过的坎这些坑我替你踩过了5.1 跨域问题为什么会浪费你一整晚前后端分离开发时前端跑在http://localhost:5173Vite默认端口后端跑在http://localhost:8080两个端口不同浏览器会判定这是跨域请求。如果你不处理你会发现所有接口请求都报CORS错误前端一个数据都拿不到。解决跨域有三条路按推荐程度排序方案一全局配置类实现WebMvcConfigurerConfiguration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }方案二在Controller类或方法上加CrossOrigin注解适合只给特定接口放行。方案三前端使用Vite代理转发。在vite.config.js中配置server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求/api/home时Vite开发服务器会把它转发到http://localhost:8080/api/home浏览器看到的始终是同源的请求自然就没有跨域问题。这个方案在开发阶段体验很好而且部署时配合Nginx的代理配置可以无缝切换。我个人在实际项目中的选择是开发阶段用Vite代理方案三部署阶段用Nginx反向代理。这样后端代码里甚至不需要写任何跨域相关的配置把跨域这件事完全交给代理层负责职责更清晰。但我还是把方案一也写出来因为如果你用的是本地开发直接请求后端地址的方式后端配置跨域就是必须的。5.2 图片上传与访问要么配好静态资源映射要么用Nginx托管博物馆系统涉及大量图片——展览封面、展品照片、轮播图。后台上传后图片到底存到哪里访问时URL怎么写这是新手最容易糊涂的地方。我的方案是上传到本地磁盘的/uploads目录然后通过配置静态资源映射让SpringBoot把/uploads/**请求映射到这个本地目录# application.yml upload: path: /usr/local/museum/uploads/Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/uploads/**) .addResourceLocations(file: uploadPath); } }此时数据库里保存的图片URL是相对路径/uploads/exhibition/20240612/xxx.jpg前端拼接上后端地址就能访问。注意路径字符串里file:后面必须紧跟绝对路径少了这个前缀的资源映射不会生效这个坑我印象很深。部署到服务器后更好的做法是由Nginx直接托管/uploads目录请求根本不需要经过SpringBoot这样静态资源的响应速度和服务器压力都会好很多location /uploads/ { alias /usr/local/museum/uploads/; expires 30d; }5.3 时间类型在前后端之间传递时的时区陷阱预约功能涉及日期和时间段比如2025-06-12 上午。前端通过Date对象处理后传给后端如果不做格式化你会在控制台看到类似2025-06-11T16:00:00.000Z这样的字符串——这是UTC格式和北京时间相差8小时。如果后端直接解析并存入数据库你会发现预约日期硬生生少了或多了一天。我的经验是统一用字符串传递时间。前端Element Plus的日期选择器el-date-picker返回的默认格式就是yyyy-MM-dd直接作为字符串传给后端后端的实体字段用String接收存入数据库的date类型字段时由数据库自动处理格式。只有在后端代码里需要做日期运算时比如判断预约日期是否早于今天、是否超过可预约范围才用LocalDate.parse()转成日期对象操作。5.4 前端打包后部署SPA路由刷新404的问题前端开发时一切正常但npm run build后把dist目录放到Nginx里刷新某个子页面居然404了。原因是Vue Router使用了HTML5 History模式页面URL里没有#号浏览器刷新时会向服务器请求/exhibition/1这样的路径而Nginx里没有这个文件自然返回404。解决方法是在Nginx配置中添加location / { try_files $uri $uri/ /index.html; }这条配置的含义是如果请求的路径不存在就回退到index.html让Vue Router接管路由解析。这是SPA部署的经典配置建议直接记下来。6. 源码使用与二次开发拿到代码后怎么跑起来、怎么改出自己的亮点6.1 从拉取源码到本地跑通的完整步骤这套系统的源码我已经打包整理过目录结构、SQL脚本、环境依赖都做了说明。拿到代码后按以下步骤操作准备环境JDK 8我建议JDK 8或11太新的版本可能和旧版Spring Boot依赖有兼容问题、MySQL 5.7、Node.js 14、Maven 3.6。创建数据库并导入脚本mysql -u root -p museum.sql脚本里包含了建库、建表和几条初始数据默认管理员账号是admin密码是admin123数据库中存储的是BCrypt加密后的结果。修改后端配置打开backend/src/main/resources/application.yml把数据库用户名密码改成你自己本地的。启动后端cd backend mvn spring-boot:run看到Started Application in xx seconds的日志就说明启动成功Swagger接口文档地址是http://localhost:8080/swagger-ui.html如果配置了。启动前端cd frontend npm install npm run dev浏览器访问http://localhost:5173就能看到门户首页。用admin/admin123登录后台地址默认是http://localhost:5173/admin。如果你在某个步骤卡住了优先检查三个方面MySQL服务是否启动了、application.yml里的端口和账号密码是否对得上、npm install是否成功。这三个问题占了本地起项目失败的九成原因。6.2 从完成毕设到答辩加分的扩展方向如果你已经把这套系统完整跑通但还想在论文里增加一些亮点我建议从下面几个方向里选一个深入方向一展品3D展示。在展品详情页接入Three.js加载GLTF格式的三维模型用户可以用鼠标拖拽旋转查看展品。后端只需要提供一个模型文件的静态资源访问路径。这个方向的实现不算复杂但视觉冲击力很强答辩演示效果极好。方向二限流与缓存优化。在预约功能上接入Redis把展览的实时余量缓存在Redis中通过DECR命令原子扣减配合定时任务把数据回写数据库。答辩时你可以准备一张图展示引入Redis前后接口响应时间的变化。方向三内容推荐。用户的浏览记录和预约记录都存在数据库里你可以做个简单的协同过滤或者基于内容相似度的推荐逻辑——用户A看过的展览类型推荐同类型的其他展览。不需要多精确但个性化推荐这个关键词放在论文里很有分量。我个人最推荐方向一原因很实际毕业设计的核心是让你把大学期间学的知识综合运用起来而Three.js的3D展示能体现你在视觉交互上的能力这在评审老师那里往往比复杂的后端性能优化更容易产生直观的印象加分。7. 最后分享一点关于毕业设计代码之外的事很多同学容易陷入一个误区系统功能越复杂越好、代码量越多越好。我在帮人看毕设源码的过程中见过太多功能堆砌但代码一塌糊涂的作品——一个管理端塞了十多个表格页面但每个页面的增删改查逻辑复制粘贴连个统一的BaseController都没抽象。功能多不是亮点功能完整且代码整洁才是。与其贪多嚼不烂不如把核心的五六张表、七八个接口做扎实。博物馆展览门户系统这个命题本身考察的就是你对SpringBoot数据层操作、Vue3组件通信、前后端联调、部署上线这套完整链路的掌握程度。把这套链路理解透你在答辩陈述时就能做到每个模块都能讲清楚为什么这么做。源码里我额外加了两个小细节一个是前端路由守卫中对登录状态的校验另一个是后端全局异常处理中针对参数校验失败的统一返回格式。这两个模块加起来不到一百行代码但能让你在前台访问受保护页面、后台提交非法参数时得到友好的提示而不是一坨看不懂的报错信息。这种对用户输入永远保持防御心态的习惯是从学生代码迈向工程代码的重要一步。最后再提个醒如果你是直接下载了别人的源码准备交差至少在答辩前把每一行核心代码都看懂能画清楚请求流程图和数据库ER图。我见过太多人拿着源码却答不出你登录之后token存在哪里你在哪一段代码里做了权限校验这类基础问题最后翻车的案例。代码可以是参考的但知识必须是你自己的。
阅读完成 · 觉得有帮助?