
做Java毕业设计每年都有大量同学在选题上纠结。管理系统做烂了电商项目又太烂大街“文化艺术活动推广系统”这个题目说实话我第一次看到时也觉得平平无奇但真正把整套东西从零到一完整走下来之后我的看法变了——这其实是一个覆盖技术面非常全、业务逻辑足够清晰、且很容易做出亮点的项目。它不仅能让你把Spring Boot和Vue的核心知识点全部串起来而且在答辩时非常有话可讲。这个系统本质上解决的是文化活动信息“发不出来”和“找不到”的问题。传统的活动通知依赖海报张贴、公众号推文信息分散且传播链路长而一套独立的推广系统可以把活动发布、分类展示、在线报名、后台审核、数据统计全部线上化连接起活动组织者和参与者两端。对于需要用Java技术栈完成毕设的同学来说它的业务复杂度刚刚好——既不会像纯CRUD那么简单导致没亮点也不会像互联网大厂那种分布式项目一样让你根本写不完。这篇文章我就基于自己实操的完整过程来拆解从技术选型、数据库设计、后端接口到前端页面、发布部署再到答辩可能遭遇的问题一步步讲清楚。无论你是刚开始准备选题还是已经建好项目正在写业务代码这篇文章都能给你一份可以直接落地的参考。1. 核心需求拆解这个“推广系统”到底在做什么文化艺术活动推广系统名字听着文艺剥开外层之后就是一个典型的内容管理外加互动报名的业务系统。理解清楚它要解决什么场景问题数据库怎么设计、接口怎么划分心里就有谱了。1.1 系统涉及的两类角色整个系统最核心的有两类用户活动组织方和普通参与者。组织方需要发布活动、维护活动信息甚至审核参与者的报名申请参与者则需要浏览活动、按分类检索、收藏感兴趣的内容、完成报名并且在个人中心看到自己的报名记录。除了这两类用户系统通常还需要一个后台管理员做全局管理包括活动内容审核、分类管理、用户管理、数据统计等。所以权限模型默认就可以拆成三种角色管理员、组织者也可以叫主办方、普通用户。如果你不想把复杂度拉太高组织者和管理员是可以合并的但分开会更好讲故事答辩时也能多讲一个“多角色权限控制”的亮点。1.2 核心业务流程拆解一个活动的生命周期大概是这样的组织者在前端创建活动 → 填写活动标题、封面、详情介绍、活动时间、活动地点、报名截止时间、人数上限 → 提交后数据进入待审核状态 → 管理员在后台审核通过后活动才会在前台公开展示 → 普通用户浏览到活动点击报名填写必要的报名信息 → 组织者或管理员在后台可以查看报名列表 → 活动开始后线下演出或展览活动结束。这里有一个关键点就是活动状态机。一个活动绝不能只用一个状态字段表示“上架/下架”那样太单薄了。更好的方式是定义一组状态0表示草稿1表示待审核2表示已通过展示中3表示已驳回4表示已结束或者已下架。这样设计出来的系统逻辑严密答辩时被老师问到“活动下架了用户还能看到吗”“审核驳回之后还能重新提交吗”这类问题时也能答得上来。1.3 附带功能模块怎么加比较合理除了活动本身我建议把路径做得完整一些加以下几个模块这会让系统功能更饱满但又不至于失控分类模块文化艺术活动分级比如音乐会、戏剧、展览、讲座、读书会等这是最简单的树形或平级分类管理后台维护。报名管理报名表、报名列表、取消报名。收藏功能用户收藏感兴趣的活动相当于“心愿单”。通知/公告系统公告或活动动态可以有但不必复杂。数据看板后台统计活动数量、报名人数趋势简单的ECharts图表即可。这些模块的核心逻辑全部围绕“活动”这一个主实体展开不会牵扯太复杂的数据关系但丰富度足够撑起一篇完整的毕业设计论文。2. 技术选型Spring Boot Vue 为什么是这个题目的最优解技术选型不能只写“我用了什么”要在答辩前想清楚“我为什么用它”。Spring Boot Vue 的前后端分离方案在应对这类系统时几乎是标准答案原因有三个。2.1 后端为什么坚定选 Spring BootSpring Boot 并不是一个比 Spring MVC 更新的框架它是 Spring 全家桶的一种更快速的使用方式。对比传统 SSM 配置地狱Spring Boot 的自动配置和约定优于配置让开发效率翻倍这点对做毕设尤其重要——你的时间应该花在业务逻辑上而不是花在写 XML 配置上。另外Spring Boot 生态太完善了。做持久层有 MyBatis、MyBatis-Plus、Spring Data JPA 可选做权限有 Spring Security 或 Sa-Token / JWT做参数校验有 Hibernate Validator。如果整个项目用 Java 实现基本不可能遇到“某个功能没有对应库要做很难”的情况。在这个系统中我推荐使用 MyBatis-Plus 做持久层。MyBatis-Plus 在 MyBatis 基础上提供了大量单表 CRUD 的现成方法对于活动、报名、收藏这类单表操作很频繁的场景能省下大量冗余代码。多表查询则自己写 XML既灵活又不至于完全丧失可控性。2.2 前端为什么选 Vue 而不是其他Vue 的优势在于学习曲线平缓同时生态完善。虽然 React 也完全可以实现同样的效果但在国内技术社区和毕业设计语境里Vue 的资料、组件库、踩坑记录都更丰富。对时间有限的学生而言这意味着你卡住时能更快搜到答案。搭建时建议使用 Vue 3 Vite Element Plus 的组合。Vite 构建速度快到肉眼可见Element Plus 组件库自带表格、弹窗、表单、上传等全套组件写后台管理界面基本是拖拽式开发体验。前台用户端页面如果想要更好看可以配合 Tailwind CSS 或自己写一套 CSS但核心还是组件库节省时间。2.3 版本搭配存在的坑版本这个问题我真的建议所有做毕设的人一开始就选对否则后面全是坑。JDK 建议用 1.8 或 11Spring Boot 用 2.7.x 系列。如果盲目跟上 Spring Boot 3.x你需要同时换 JDK 17、换 javax 到 jakarta 命名空间MyBatis-Plus 也要用适配版本很多老教程里的代码会直接报错。没有必要为了新而新毕业设计追求的是可靠和完整。前端方面Node.js 版本不要太老建议 16 或 18。Vue 3 搭配 Vite 4/5 问题不大。Element Plus 要和 Vue 3 配套千万别拿 Element UI那是 Vue 2 专属装进 Vue 3 项目里导入时会直接报类型错误。实操心得如果你在建项目时发现 idea 创建 Spring Boot 项目超时不要死磕直接去 Spring Initializr 网站下载压缩包再导入。本地网络访问国外服务不稳定这是最容易卡住的第一个环节。3. 数据库设计ER 图和表结构背后的思考过程数据库设计是答辩时老师最爱深挖的部分也是整个系统能不能顺利开发的地基。我按照实际的表拆分方式逐个讲解每一张表的用途和设计时要注意的关键点。3.1 核心表结构活动、用户、报名首先是用户表sys_user需要包括用户名、密码加密存储、昵称、头像、手机号、角色role字段区分管理员/组织者/普通用户、创建时间、状态。密码必须用 BCrypt 加密绝对不能明文存储。然后是活动表activity会有一个归属组织者IDuser_id、活动标题、封面图URL、活动类型关联分类表、活动详情描述、活动地点、开始时间、结束时间、报名开始/截止时间、人数上限、当前报名人数、状态字段、浏览量、审核驳回原因等。报名表activity_signup相对简单ID、活动ID、用户ID、报名时间、报名状态已报名/已取消/已核销、附加信息比如带几个人。核心约束是同一用户对同一活动不能重复报名——代码里可以做校验数据库层面建议加唯一索引activity_id, user_id做双保险。这三张表加上分类表category、收藏表activity_favorite基本就覆盖了系统的所有业务数据。3.2 状态字段和外键关系的设计选择在设计时我发现很多学生喜欢把状态字段做成字符串比如“待审核”“已通过”。我更推荐用 int 或者 tinyint 存数字在 Java 代码里定义枚举来翻译。好处是数据表更干净检索更快也不容易因为手滑打出别字导致查不到数据。关于外键我的建议是逻辑关联、不建物理外键。也就是说在活动表里有 user_id 字段但不会真的用 FOREIGN KEY 去约束。物理外键在数据量上来之后会影响插入和更新性能而且级联删除很容易误伤数据。只要在 Java 代码里保证通过 user_id 去查用户表逻辑上一致就可以了。这一点答辩时如果老师问起来也算是一个可以深入聊的设计决策。3.3 页面上可能出现的字段别漏掉有几个字段我一开始漏了后来返工补上的在这里列一下提醒想做的朋友活动详情建议直接用富文本编辑器的 HTML 内容存储字段类型用 mediumtext 或 longtext。阅读量/浏览量活动列表页可以顺便展示“多少人看过”提升交互感其实实现只是一次 update 自增的事。逻辑删除所有表都加一个 deleted 字段0正常1删除用 MyBatis-Plus 的 TableLogic 逻辑删除配置。做毕业设计用物理删除也没啥大毛病但能写逻辑删除的话在论文里也是一个加分项。创建时间/更新时间所有业务表统一加 create_time 和 update_time后端用 MyBatis-Plus 的自动填充不用每个接口手动维护时间。实现的关键代码如下展示一个基础实体的写法参考Data TableName(activity) public class Activity { TableId(type IdType.AUTO) private Long id; private Long userId; private Long categoryId; private String title; private String cover; private String detail; private String location; private LocalDateTime startTime; private LocalDateTime endTime; private LocalDateTime signupStartTime; private LocalDateTime signupEndTime; private Integer maxPeople; private Integer currentPeople; private Integer status; private Integer viewCount; TableLogic private Integer deleted; TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }4. 后端核心实现接口怎么拆、权限怎么做、业务逻辑写在哪后端部分是整个项目的安全底线。如果把业务逻辑全部堆在 Controller 里短时间能跑通但后续想加功能就会非常痛苦。合理的分层是 Controller → Service → MapperController 只做参数接收和结果返回Service 写业务逻辑Mapper 管 SQL。4.1 统一返回结构和全局异常处理前后端分离的项目接口返回的结构一定要统一。我的做法是比较经典的三段式结构{ code: 200, message: 操作成功, data: {} }所有的查询、新增、更新接口都返回这个结构前端在 request 拦截器里统一判断 code。code 为 200 就取 data 渲染不为 200 就弹出 message 错误提示。这样就完全避免了前端每个接口分别处理成功/失败逻辑的重复劳动。全局异常处理我用了 RestControllerAdvice在 Service 里凡是遇到业务上不该发生的情况直接 throw new BizException(活动报名人数已满)然后在全局异常处理器里统一转成返回结构并设置 code 为 500。这样代码中不会到处是 try-catch逻辑读起来干净非常多。注意统一异常处理别忘了处理参数校验异常 MethodArgumentNotValidException以及没有权限时抛出的异常否则前端拿到的结构永远不统一排查问题时非常痛苦。4.2 登录认证与权限控制的落地方式登录这块我选择用 JWT 拦截器方案没有引入完整的 Spring Security。Spring Security 功能强大但配置太多对毕设项目来说学习成本和调试成本都偏高。JWT 方案轻量、易理解、代码可控而且非常容易讲清楚。大致的实现链路是这样的用户输入用户名密码后端用 BCrypt 匹配密码成功后生成 JWT token返回给前端。前端把 token 存到 localStorage并在 axios 请求拦截器中加入Authorization: Bearer token请求头。后端写一个拦截器HandlerInterceptor拦截除了/api/auth/login、/api/activity/list、/api/activity/detail等少数公开接口外的所有请求。拦截器里解析 token如果解析成功就把用户ID和角色放入 ThreadLocal方便后续 Controller 或 Service 直接获取当前登录用户。对于只有管理员或组织者能用的接口写一个自定义注解 RequireRole在拦截器里做角色判断。核心拦截器代码大致是这个样子public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } // 检查是否跳过登录 HandlerMethod handlerMethod (HandlerMethod) handler; // 这里可以判断 PassToken 注解有就直接放行 String token request.getHeader(Authorization); if (token null || token.isEmpty()) { throw new BizException(未登录); } // 解析 token 得到 userId 和 role放入 UserContext UserContext.set(userId, role); return true; } }这样一套下来权限模型就完整了。答辩演示的时候你可以分别用管理员、组织者、普通用户三个账号登录展示同一接口在不同角色下的不同表现非常有说服力。4.3 活动发布的完整业务闭环活动发布的 Service 层逻辑应该是整个后端中最复杂的一段也是值得在论文中重点着墨的部分。我的实现顺序如下创建活动时先校验当前登录用户角色必须是组织者或管理员然后填充活动基础字段初始状态设为待审核插入数据库。组织者可以修改状态为待审核但尚未审核通过的活动一旦审核通过则不能随意修改必须走“申请修改”流程或由管理员直接驳回。管理员审核调用的接口逻辑用事务包起来查询活动ID、判断当前状态必须是待审核、然后把状态改为已通过或已驳回驳回时必须填写驳回原因。用户报名活动的逻辑是校验活动存在、校验状态为审核通过、校验当前时间在报名时间范围内、校验当前报名人数小于人数上限然后开启事务新增报名记录同时更新活动表当前报名人数加一。这里特别要注意并发问题。两个用户同时报名一个剩余最后1个名额的活动如果代码是“先查人数再判断再插入”就会出现超卖。当然毕设项目并发量极低但可以在论文中讨论这个问题并用数据库行锁或者乐观锁来解决也就是说需要让 update activity set current_people current_people 1 where id ? and current_people max_people 这种原子 SQL。能做到这一步就是对业务有深度的理解。4.4 文件上传与富文本处理的常用方案文化活动系统必然要处理封面图和活动详情里的图片。最简单的方案是后端用 MultipartFile 接收文件保存到本机磁盘的上传目录再把访问路径返回给前端。但要注意前端访问图片时需要一个静态资源映射配置否则把文件存到磁盘上却访问不到白忙一场。Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceHandler(file: uploadPath /); } }如果你的服务器或电脑上不方便长期存文件也可以换成阿里云 OSS 或腾讯云 COS把文件传到对象存储拿到 URL 直接入库。这个对于毕设来说不强制但做出来也是加分项。富文本编辑器推荐 wangEditor它支持图片上传到自己的后端接口在 vue 项目中集成比较简单默认会输出 HTML 内容正好可以直接存到文章的 detail 字段。5. Vue 前端实现要点从页面搭建到交互细节前端部分虽然看起来只是“画页面”但真正写起来也有很多细节。这里我从项目结构、路由权限、核心页面、组件封装四个维度来讲。5.1 前端项目结构建议的项目目录是这样的src/ ├── api/ # 所有接口请求封装 │ ├── activity.js │ ├── user.js │ └── signup.js ├── assets/ # 静态资源 ├── components/ # 通用组件活动卡片、分页、上传组件 ├── router/ # 路由配置 ├── store/ # Pinia 状态管理登录状态、用户信息 ├── views/ │ ├── home/ # 前台首页 │ ├── activity/ # 活动列表、活动详情 │ ├── user/ # 个人中心、我的报名 │ ├── admin/ # 后台管理布局和页面 │ └── login/ # 登录注册 └── utils/ # 请求封装、工具函数这种结构的好处是每个文件职责单一后期加功能时不需要在巨型文件里来回搜索。很多毕业生的前端代码往往把接口请求直接用 axios.post 写在组件里导致组件非常臃肿。抽一层 api 封装出来后期维护会舒服很多。5.2 路由守卫与登录态管理前端必须做路由守卫否则用户直接输入后台管理页面的 URL 就能跳过登录查看了这个在答辩时被指出来会很尴尬。用 Pinia 存登录状态在全局前置守卫中判断router.beforeEach((to, from, next) { const userStore useUserStore() if (to.meta.requiresAuth !userStore.token) { next(/login) } else if (to.meta.role to.meta.role ! userStore.role) { next(/403) } else { next() } })同时axios 响应拦截器需要处理 token 过期的情况。我这边后端对 token 设置了 24 小时过期时间前端拦截到 code 为 401 时清空本地登录信息并跳回登录页。这一步如果不做就会出现“页面还在但数据刷新失败”的尴尬体验。5.3 活动列表、详情页与报名功能的前端逻辑活动列表页建议用卡片列表 分类标签筛选 分页组件。每个活动卡片至少展示封面、标题、时间、地点、报名人数/人数上限。前端的分页参数页码、每页条数、分类ID作为 query 参数传给后端后端返回分页结构。活动详情页是交互最丰富的页面。需要展示完整的活动信息并根据状态显示不同的按钮——未开始时显示“立即报名”报名时间已截止显示“报名已截止”已报名则显示“已报名/取消报名”活动结束显示“活动已结束”。这些状态判断最好封装成计算属性不要写一长串 if-else 堆在模板里。报名弹窗里的表单可以用 Element Plus 的 Dialog 加 Form 实现需要设置校验规则必填项、手机号格式。真正重要的逻辑是后端对当前用户是否已报过名的校验前端在初始化详情时就应该请求一个接口拿到“当前用户对该活动的报名状态”这样按钮才能提前渲染正确。5.4 后台管理界面的模块划分后台管理的核心页面就是几个表格页配上新增/编辑弹窗和删除确认。活动管理的表格展示活动标题、分类、所属组织者这里需要关联查询用户名、状态、报名人数/人数上限、创建时间。操作列放审核按钮、上下架按钮、查看详情按钮。我用 Element Plus 的 el-table 几个组件就能快速完成确实是很成熟的方案。比较需要留心的是状态展示写一个 formatStatus 函数把数字状态转成带颜色的 tag审核通过用 green tag待审核用 orange tag驳回用 red tag这样后台看状态一目了然。6. 常见问题与排查心得这些坑我帮你踩过了这个部分才是真正从实操中长出来的经验。我在开发这个系统时遇到了不少诡异问题逐个说下怎么定位、怎么解决。6.1 跨域问题前后端分离必然遇到跨域。如果你用 Vite 开发服务器最快捷的方案是配置代理让前端/api开头的请求转发到http://localhost:8080这样就绕过了浏览器的跨域限制。具体在 vite.config.js 中写server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }生产环境中如果前后端部署到同一个域名下就不存在跨域问题了。需要区分开发环境和生产环境的环境变量不要写死。6.2 MyBatis-Plus 表不存在时自动建表有人提到 springboot mybatis 表不存在自动建表。MyBatis-Plus 本身没有这个功能但如果你用 Spring Boot 连接 MySQL可以在数据源 URL 上加参数jdbc:mysql://localhost:3306/art_system?createDatabaseIfNotExisttrue这个参数能让 MySQL 在库不存在时自动创建数据库。表不存在时则不能依赖框架要么自己执行 SQL 脚本要么引入 liquibase 或 Flyway 做数据库版本管理。对毕设来说直接提供一份 init.sql 建表脚本就够了但知道这个参数能在答辩时展示你考虑问题的全面性。6.3 JWT token 失效与用户并发刷新实际操作中前端经常遇到一个问题token 明明没过期但刷新页面后用户信息丢了。原因是刷新后 Pinia 的状态是被重新初始化的token 从 localStorage 能拿到但用户信息没有重新拉取。解决方案是在路由守卫里判断“有 token 但 store 里没有用户信息”就调用/api/user/info重新拉取。6.4 大文件上传超时如果活动详情里上传超大的图片或视频默认的上传大小限制会直接报错。Spring Boot 默认上传大小为 1MB需要修改配置spring: servlet: multipart: max-file-size: 50MB max-request-size: 50MB但如果真的在页面上插入一个几百MB的视频后端转发的方式就会很吃力。更合理的做法是前端直接用 OSS 客户端直传或者使用对象存储的预签名 URL 上传。配置了这些也能成为论文中“系统优化”的亮点。6.5 活动报名人数并发超卖前面提过的并发问题这里补充下具体的落地方式。一个稳妥的方案是用数据库行锁UPDATE activity SET current_people current_people 1 WHERE id #{activityId} AND current_people max_people如果更新受影响行数为 0说明名额满了直接抛异常。然后再插入报名记录。这个 SQL 是把“检查名额并扣减”合并成了一个原子操作即使高并发场景也不会超卖。配合事务里先执行 update 再 insert 的顺序基本可以杜绝问题。我实际项目在毕设答辩这种量级下当然用不到这么极端的处理但把这套逻辑写进论文里水平马上就和其他同学拉开了。7. 部署上线与项目扩展建议项目的最后一步是部署也有不少同学卡在这里。我先说本地跑通的方法再说扩展方向。7.1 本地启动的关键步骤后端部分先在 application.yml 里配置好 MySQL 的连接地址、用户名密码。然后运行主启动类观察控制台日志确认启动成功。前端部分在项目根目录执行 npm install 安装依赖执行 npm run dev 启动开发服务器浏览器访问 localhost:5173。如果要部署到服务器推荐的组合是前端打包后交给 Nginx 托管静态文件后端打包成 jar 后用 java -jar 运行MySQL 直接安装在服务器或用 Docker 起一个。Nginx 需要将 /api 开头的请求反向代理到后端端口server { listen 80; server_name your-domain.com; root /usr/share/nginx/html; index 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; } location /upload/ { alias /var/art-system/upload/; } }7.2 项目后续可以怎么扩展做完基础系统后如果时间和精力允许有几个方向可以提升项目的含金量。第一个是引入 Redis 做活动详情的缓存降低数据库压力同时把浏览量用 Redis 的 increment 来统计。第二个是增加消息通知功能报名审核结果、活动状态变更时给用户发站内信或邮件通知用 Spring Boot 整合 Kafka 或 RabbitMQ 来做。第三个是增加基于标签的活动推荐也就是简单的根据用户收藏和报名历史计算相似度推荐可能感兴趣的活动。我个人的实际体会是这类系统最花时间的往往不是框架层面的问题而是业务边界想不清楚。做之前先把状态机、角色、业务流程在纸上画清楚开发效率会高很多后期也不容易推翻重来。最后再分享一个小技巧答辩时不要只演示“能用”要准备一两张数据表和核心流程的截图打开 IDEA 从 Controller 点到 Service 再到 Mapper展示你对代码结构是真正理解的。这些细节加起来比堆砌多少新技术名词都更有说服力。希望这篇文章对做这个题目的朋友有帮助。