做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 Boot
Spring 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 或 11,Spring 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),会有一个归属组织者ID(user_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 → Mapper,Controller 只做参数接收和结果返回,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?createDatabaseIfNotExist=true这个参数能让 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,展示你对代码结构是真正理解的。这些细节加起来,比堆砌多少新技术名词都更有说服力。希望这篇文章对做这个题目的朋友有帮助。