1. 从毕设选题到技术选型:为什么是SpringBoot + Vue + B/S
每年毕业季,计算机专业的同学都在纠结同一个问题:毕设做什么题目才既不容易翻车,又能让答辩老师觉得有工作量?我见过太多人一开始选了个听起来高大上的方向,比如人工智能图像识别、分布式推荐系统,结果做到一半发现数据凑不齐、环境配不好、模型跑不动,最后两个月全耗在调环境上。相比之下,"基于SpringBoot + Vue的校园活动管理系统"这个题目,踩中的恰恰是"稳"字。
先说为什么这个组合是当下的黄金搭配。SpringBoot解决了传统SSH、SSM框架里复杂到劝退的XML配置问题,它用"约定优于配置"的思路把项目启动成本降到了极低,内置Tomcat、自动装配、起步依赖,写一个Controller就能跑起来。而Vue作为前端框架,采用组件化开发,数据驱动视图,配合Element UI这类组件库,做后台管理页面基本就是拼积木的效率。两者组合起来就是目前最主流的前后端分离开发模式:Java后端提供JSON接口,前端Vue负责渲染页面和交互,分工清晰,一个人也能兼顾两头。
再说B/S架构。可能有人会问,为什么不选C/S架构,比如用Java Swing或JavaFX做个桌面客户端?理由其实很实际:校园活动管理系统天然需要"多角色、多地点、免安装"的使用场景。学生要能在宿舍、图书馆、手机浏览器上随时查看活动,社团负责人要能在不同电脑上发布活动,管理员要能在办公室审核,如果做C/S架构,每个终端都得装客户端,维护成本直接爆炸。B/S架构只需要一个浏览器就能访问全部功能,后台升级也只需更新服务器,用户侧完全无感知。这个决策本身也是答辩时可以说清楚的"架构选型理由"。
从课题规划的角度看,这个题目还有一个隐形优势:功能边界清楚,数据模型直观。校园活动的核心业务无非就是活动发布、活动报名、活动审核、公告通知、用户管理这几条线,每条线都能对应到数据库表结构和接口设计上,工作量可控,也方便在中期答辩时展示阶段性成果。如果你在此基础上加一点"加分项",比如活动签到二维码、报名人数图表统计、消息提醒,又能让项目在同题目的同学中显得更有亮点。后面的章节我会把这些加分项怎么落地一并拆开讲。
2. 数据库是地基:活动管理系统的核心表设计与关系梳理
说实话,我评审过不少学生的毕设代码,最大的问题往往不是功能写不出来,而是数据库设计一塌糊涂——有的把所有信息塞进一张表,有的字段命名随意到只有自己能看懂。校园活动管理系统虽然业务不算复杂,但涉及用户、角色、活动、报名、公告、分类等多个实体,表结构设计得好不好,直接决定后端代码写起来是顺滑还是痛苦。
2.1 核心表的结构与职责划分
按照"一个业务实体一张表"的原则,这个项目至少要拆出下面这些核心表:
| 表名 | 核心字段(举例) | 作用 |
|---|---|---|
| user | id, username, password, real_name, role, college, phone, avatar | 存储学生、社团负责人、管理员三类用户 |
| activity | id, title, category_id, organizer_id, description, location, start_time, end_time, max_people, status, cover_image | 活动主体信息,含时间地点、人数上限、审核状态 |
| activity_category | id, name, sort | 活动分类,如学术讲座、文体竞赛、志愿服务 |
| sign_up | id, activity_id, user_id, sign_time, status | 报名记录,用户与活动是多对多关系 |
| announcement | id, title, content, admin_id, create_time, is_top | 公告通知,可置顶 |
| audit_log | id, activity_id, operator_id, action, comment, create_time | 审核轨迹,记录谁在什么时候做了什么审批 |
用户表里我特别加了role字段用来区分三类角色,简单直接,权限控制用字符串比较就能完成,毕设阶段不需要引入Spring Security里那套复杂的角色继承体系,但建议用整数类型(0表示管理员,1表示社团负责人,2表示学生),这样扩展性更好。
2.2 为什么活动表和报名表必须分开
我见过有同学把报名学生直接存成活动表里的一个逗号分隔字符串字段,这样做眼前确实省事,查一查活动就把所有人都拿到了。但它有两个致命问题:一是当你想查"某个学生参加过哪些活动"时,必须遍历所有活动再拆字符串,性能和代码复杂度都很差;二是无法记录报名时间、取消报名等状态变化。
正确的做法是拆一张sign_up关联表,里面存activity_id和user_id两个外键,再加status字段表示报名状态(0已报名,1已取消,2已签到)。这样既支持活动详情页显示报名人数,也支持个人中心查看我的活动列表,还能延伸出签到功能——把status改成2就是签到。所有业务查询都变成简单的SQL条件过滤,这就是关系型数据库该有的用法。
2.3 活动审核状态机的设计
校园活动的发布不能是"学生发什么就上什么",必须经过管理员审核,所以活动表里的status字段非常关键。我建议的状态值这样设计:
- 0:待审核(社团负责人提交后)
- 1:已通过(管理员审核通过,前端可见)
- 2:已拒绝(管理员驳回,需填写拒绝原因)
- 3:已结束(活动时间超过end_time后自动或手动置为结束)
- 4:已取消(管理员或发起人在特定情况下取消)
为什么要把"已结束"单独设计一个状态,而不是在前端按时间判断?因为前端时间判断依赖本地时钟,用户篡改本地时间可能会出现异常显示;而且在管理后台,统计"我办过几个已结束的活动"这类报表时,直接查status比每次比较时间要高效得多。你完全可以在后端用一个定时任务,每半小时扫描一次活动表,把end_time小于当前时间且status为1的活动自动改成3,这个逻辑写起来不超过十行,但答辩时讲出来就是一个很实在的亮点。
2.4 数据库初始化与测试数据的策略
开发阶段我推荐用spring.sql.init.mode=always配合schema.sql和data.sql自动建表和初始化数据,这样任何一台电脑拉下来项目启动就能跑,不用手动导SQL。不过要注意:schema.sql里建表语句建议用CREATE TABLE IF NOT EXISTS,否则重启项目时会因为表已存在而报错;data.sql里的管理员账号记得用BCrypt加密后的密码,不要明文存。
测试数据也要刻意构造:至少造20条状态不同的活动、10个以上用户、每个活动配几个报名记录,这样前端分页、筛选、统计图表都有数据可展示。很多同学项目演示时页面空荡荡,不是功能没写完,是数据没造够,这个细节很容易被忽略,但演示效果差别巨大。
3. SpringBoot后端的模块划分与关键接口实现
数据库表设计好之后,后端开发的核心任务就是把这套数据模型暴露成前端能够消费的HTTP接口。SpringBoot项目虽然脚手架一拉就出来,但包结构如果不规划好,写着写着就会变成一锅粥。我先给出一套我实测下来很顺手的包结构,再拆解几个核心接口的实现思路。
3.1 分包结构与统一响应格式
com.campus.activity ├── controller // 接口层,只做参数接收和结果返回 ├── service // 业务逻辑层,处理具体业务规则 ├── mapper // 数据访问层,MyBatis接口 ├── entity // 数据库实体类 ├── dto // 前端传参和返回给前端的对象 ├── config // 配置类:跨域、拦截器、日期格式化等 ├── common // 通用类:统一返回结果、异常处理、常量 └── utils // 工具类:JWT工具、密码加密工具这里有个容易被新手忽略的点:entity(数据库实体)和dto(数据传输对象)一定要分开,不要图省事直接用实体类接前端参数。原因是实体字段往往比前端需要的多(比如密码、创建时间),直接暴露给前端有安全隐患,而且当接口需要组合多个表的数据时,一个实体类根本接不住。我习惯在dto包里建ActivityCreateDTO、ActivityQueryDTO这类对象,字段按接口实际需要来定义,用Hutool的BeanUtil工具一行代码完成对象拷贝。
统一响应格式我用的是一个最简单的Result<T>类,包含code(200成功,500失败)、message(提示信息)、data(业务数据)三个字段。所有Controller方法都返回这个类型,前端Axios响应拦截器里统一判断code,不需要每个接口单独处理错误分支。这个设计虽然简单,但能让整个前后端联调过程清爽很多。
3.2 活动发布与分页查询:最核心的业务接口
活动发布接口是社团负责人的主要操作,它的业务规则包括:活动标题不能为空,活动时间必须在当前时间之后,活动地点必填,封面图可以后补。这些校验我建议用@Validated注解加JSR 303校验注解实现,比如@NotBlank、@NotNull,而不是在Service里写一长串if判断。这样代码更简洁,校验逻辑也统一管理。
举个例子,创建活动的Controller方法大致是这样:
@PostMapping("/activity") public Result<String> createActivity(@RequestBody @Valid ActivityCreateDTO dto) { activityService.createActivity(dto); return Result.success("发布成功,等待管理员审核"); }然后createActivity的业务逻辑里,除了把DTO拷贝成实体插入数据库,还有一个容易被忽略的步骤:设置初始状态为0(待审核)。有些同学会忘记这个字段,默认值没设置,数据库里就是null,导致前端筛选活动时那些记录直接消失,排查半天发现是状态字段为空。我建议在数据库建表时就把status字段设为DEFAULT 0,双保险。
分页查询接口是列表页的生命线。这里我用MyBatis Plus的Page分页插件,配合LambdaQueryWrapper做条件拼接:
public Page<ActivityVO> pageActivities(ActivityQueryDTO dto) { LambdaQueryWrapper<Activity> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StringUtils.isNotBlank(dto.getStatus()), Activity::getStatus, dto.getStatus()) .like(StringUtils.isNotBlank(dto.getTitle()), Activity::getTitle, dto.getTitle()) .eq(dto.getCategoryId() != null, Activity::getCategoryId, dto.getCategoryId()) .orderByDesc(Activity::getCreateTime); return activityMapper.selectPage(new Page<>(dto.getPageNum(), dto.getPageSize()), wrapper); }用eq带条件参数的方式,前端传什么就按什么筛选,不传就不拼这个条件,既安全又灵活。返回给前端的VO里还可以用@TableField(exist = false)标记一个虚拟字段registrantCount,通过子查询统计报名表里对应活动的报名人数,列表页就能直接显示每个活动的参与热度。
3.3 报名与取消报名:并发安全的细节处理
报名接口表面上就是一个INSERT操作,但如果不加控制,会出现一个严重问题:活动名额50人,第51个人依然能报名成功,因为两个请求同时通过了"当前人数 < maxPeople"的检查。解决这个问题有三种常见方案:
第一种是数据库层面加锁,在活动表增加一个version字段做乐观锁,更新时检查版本号;第二种是在sign_up表给activity_id和user_id加唯一索引,从根上防止一个人重复报名;第三种是使用SELECT ... FOR UPDATE对活动记录加行锁,先锁再查再插入。
毕设项目里我推荐第二种方案加第一种方案的简化版组合:先给sign_up表加唯一索引,然后在Service层用synchronized或者分布式锁都显得过度设计,更稳妥的是直接对活动ID加数据库行锁:
@Transactional public void signUp(Long activityId, Long userId) { Activity activity = activityMapper.selectByIdForUpdate(activityId); if (activity == null) { throw new BusinessException("活动不存在"); } if (activity.getStatus() != 1) { throw new BusinessException("活动未通过审核,无法报名"); } int count = signUpMapper.countByActivityId(activityId); if (count >= activity.getMaxPeople()) { throw new BusinessException("活动名额已满"); } signUpMapper.insert(...); }selectByIdForUpdate在MySQL的InnoDB引擎下会对该行记录加排他锁,直到事务提交才释放,这样并发的报名请求就会排队执行,名额不会超。这个方案兼顾了正确性和代码可读性,答辩时如果有老师问到"并发场景下怎么保证报名人数不超",你就可以把这套方案完整讲出来,这就是项目中的亮点。
3.4 登录认证与权限拦截:JWT的轻量实践
校园活动管理系统不需要引入Spring Security全家桶,那套配置对毕设来说太笨重。我更推荐JWT(JSON Web Token)方案:用户登录成功后,后端生成一个Token返回给前端,前端存在localStorage里,每次请求在Header里带上Authorization: Bearer <token>,后端通过拦截器统一解析。
拦截器的核心逻辑是:从请求头取出Token,用io.jsonwebtoken.JJWT库解析,解析成功就把用户ID和角色放进ThreadLocal里,供Controller层直接用;解析失败或没有Token,直接返回401状态码。需要注意的是JWT的密钥不要写死在代码里,放到application.yml里,用@Value注入,方便不同环境切换。
权限控制方面,我用一个简单注解@RequireRole("ADMIN")标注需要管理员权限的接口,然后在拦截器里通过反射读取HandlerMethod上的注解,比对当前用户的角色。这套轻量方案不需要引入额外依赖,代码量也不多,却能实现和Spring Security类似的"声明式权限控制"体验。
4. Vue前端从零搭建:组件拆分、路由守卫与接口联调
后端接口就绪后,前端Vue项目的开发重点在于:项目结构怎么组织、页面如何拆组件、接口怎么封装、登录态怎么控制。很多同学前端喜欢一把梭,App.vue里面堆几百行代码,最后想改一个按钮都要在代码海里捞。这一章我按实际开发顺序,把前端的关键环节逐步拆解。
4.1 项目初始化与目录规范
Vue项目的创建我建议直接用Vite,相比vue-cli,Vite冷启动速度快得不是一点半点,尤其适合毕设这种需要反复调试的场景。创建命令很简单:
npm create vite@latest campus-frontend -- --template vue装好基础依赖后,建议额外安装这三个核心库:vue-router(前端路由)、pinia(状态管理)、axios(HTTP请求)。Element UI组件库用element-plus,图标库用@element-plus/icons-vue,表格、表单、弹窗、上传组件一应俱全,能省下大量写CSS的时间。
目录结构我这样规划:
src ├── api // 每个模块的接口请求函数,如activity.js, user.js ├── assets // 静态资源 ├── components // 通用组件,如分页组件、图片上传组件 ├── router // 路由配置 ├── stores // Pinia状态管理,主要存用户信息 ├── utils // 工具类,如axios实例封装 ├── views // 页面组件,按业务模块分文件夹 │ ├── login │ ├── home │ ├── activity │ ├── user │ └── admin └── App.vue4.2 Axios封装与Token拦截器:联调期省一半力气的关键
很多同学在联调时最头疼的问题就是每个接口都要手动处理错误弹窗、手动拼接Token,代码重复一大堆。正确做法是在utils/request.js里创建一个Axios实例,统一配置基础URL和超时时间,然后在请求拦截器里加Token,在响应拦截器里统一处理业务错误和登录过期。
import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:带上Token request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) // 响应拦截器:统一处理错误 request.interceptors.response.use( response => { const res = response.data if (res.code === 200) { return res } ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) }, error => { if (error.response?.status === 401) { ElMessage.error('登录已过期,请重新登录') localStorage.removeItem('token') router.push('/login') } else { ElMessage.error('网络异常,请稍后重试') } return Promise.reject(error) } )每次新增接口,只需要在api/activity.js里写一行函数:
import request from '@/utils/request' export const getActivityPage = (params) => request.get('/activity/page', { params }) export const createActivity = (data) => request.post('/activity', data) export const signUpActivity = (activityId) => request.post(`/activity/${activityId}/signup`)这样Controller层的路径变更也只需改这一处,全项目替换成本极低。
4.3 路由守卫与角色权限控制
路由设计上,我划分了两大块:前台页面(活动列表、活动详情、个人中心)和后台管理页面(用户管理、活动审核、公告管理)。后台管理页面不能直接通过URL访问,必须有管理员权限,这就需要用到Vue Router的全局前置守卫:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}') if (to.path === '/login') { next() return } if (!token) { next('/login') return } if (to.meta.requiresAdmin && userInfo.role !== 0) { ElMessage.error('无权访问该页面') next('/') return } next() })后端接口的权限校验是安全第一道防线,前端路由守卫是体验第二道防线,二者缺一不可。后端拦截器保证接口不能被越权调用,前端路由守卫保证普通用户不会看到管理页面入口、也不会因为手动改URL跳进去出现一片白屏。
4.4 活动列表页与详情页的组件拆分实践
活动列表页是系统里访问量最高的页面,我把它拆成三个子组件:ActivityFilter(筛选条件栏)、ActivityCard(单个活动卡片)、Pagination(分页组件)。列表页本身只负责管理筛选状态和调用接口获取数据,数据流向清晰。
活动卡片组件ActivityCard接收一个activity对象作为props,内部展示封面图、标题、时间、地点、报名人数这些信息,底部根据status动态渲染不同操作按钮:
- status为1(已通过):显示"报名"或"已报名"按钮和"查看详情"
- status为0(待审核):如果是发起人自己,显示"审核中"
- status为2(已拒绝):显示拒绝原因
这里的按钮状态判断最好用计算属性实现,而不是在模板里写一堆v-if嵌套,代码可读性会好很多。
活动详情页则用vue-router动态路由传参,路径是/activity/:id,页面加载时根据id调用详情接口。详情页的报名按钮需要注意一个细节:如果当前用户已经报名过,按钮要变成"已报名"且置灰,避免用户重复提交。这个判断可以通过调用详情接口时后端一并返回signUpStatus字段,也可以前端在页面加载时查询一次报名状态,推荐前者,少一次请求。
4.5 图片上传与富文本:两个隐藏的复杂点
活动封面图上传和公告富文本编辑,是两个看起来很基础但实现起来容易卡壳的功能。
图片上传我用Element Plus的el-upload组件,设置action指向后端的/api/upload接口,后端用MultipartFile接收文件,保存到服务器的/uploads目录,然后把访问URL返回给前端。这里有一个坑:Element Plus的el-upload组件默认用XHR上传,而DevServer的代理配置如果在vite.config.js里没有正确配置/api前缀,上传请求就会404。具体配置后面部署章节会详细讲。
富文本我推荐wangeditor这个开源库,国内文档齐全,中文使用习惯也友好。后端直接接收富文本编辑器生成的HTML字符串存库。需要注意XSS安全问题,最好在后端用一个简单的HTML过滤工具,把<script>标签剔除掉,避免用户在公告里注入脚本。这个细节在答辩时提一下,老师会觉得你考虑问题很全面。
5. 前后端联调中我最想提醒的三个坑
联调阶段是毕设项目最磨人的阶段,前后端各自单独跑都没问题,一联起来就各种诡异。这里我把自己踩过并且帮学生排查过的三个高频坑拎出来,每一个都是真实场景,照着排查能省一两天时间。
5.1 跨域问题:报错里最常见的那条红色消息
前后端分离项目必然遇到跨域。你在Vue项目里用Axios请求http://localhost:8080/api/activity,浏览器会直接拦截响应,控制台报"Access-Control-Allow-Origin"错误。解决跨域一般有两种方案。
方案一:后端开启CORS跨域配置,在SpringBoot里写一个WebMvcConfigurer配置类,允许所有来源(或者指定前端地址)跨域访问。这种方式适合部署上线时使用。
方案二(开发推荐):用Vite的代理功能,在vite.config.js里配置:
export default defineConfig({ server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })这样前端页面里访问/api/activity,Vite DevServer会代理转发到后端8080端口,浏览器看到的请求是同源的,完全不会有跨域问题。用代理方案时,Axios的baseURL要设置成/api,而不是完整地址。
5.2 日期格式化问题:JSON里的时间戳到前端变成了看不懂的数字
SpringBoot默认序列化LocalDateTime会输出数组或时间戳格式,前端拿到后显示成1715652000000这样的数字,直接渲染到页面上非常丑。解决办法是在application.yml里配置全局日期格式:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8配置后后端返回的时间就是2024-05-14 20:00:00这种人类友好格式。但注意,前端如果用的是el-date-picker日期选择器,选完日期提交给后端时是2024-05-14T20:00:00这种ISO格式,需要在提交前用Day.js转一下格式:
import dayjs from 'dayjs' const formattedTime = dayjs(value).format('YYYY-MM-DD HH:mm:ss')这类细节不处理,就会出现"明明选了时间,提交后数据库里却是null"的诡异现象。
5.3 文件上传大小限制:前端传图一直失败
SpringBoot默认的单文件上传大小上限是1MB,活动封面图动辄两三MB,一传就报FileSizeLimitExceededException。不管你在前端怎么调组件配置,只要后端没放开这个限制,永远传不上去。在application.yml里加上:
spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB同时,Nginx部署时也要同步调整client_max_body_size配置,否则上线后同样会踩坑。这是开发环境能跑、部署到服务器就报错的最典型原因之一。
6. 部署上线与答辩准备的实战清单
6.1 前后端分离部署:Nginx既是Web服务器也是反向代理
项目功能全部完成后,部署是检验工程化能力的最后一关。我推荐的部署架构是:后端SpringBoot打成Jar包运行在服务器的某个端口(比如8080),前端Vue执行npm run build生成dist静态目录,然后由Nginx同时承担静态文件服务和请求转发的角色。
Nginx配置的关键片段如下:
server { listen 80; server_name your-domain.com; # 前端静态资源 root /var/www/campus-frontend/dist; index index.html; # 解决Vue Router history模式刷新404问题 location / { 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; client_max_body_size 20m; } # 上传文件访问 location /uploads/ { alias /var/www/campus-backend/uploads/; } }三个location各司其职:根路径返回前端页面,/api开头的请求转发给后端,上传的图片通过alias映射到服务器磁盘目录。这套配置里最容易踩坑的就是try_files $uri $uri/ /index.html这一行——如果不加,用户在前端页面刷新路由时会出现404,因为Vue Router的history模式需要服务器把所有非接口请求都重定向到index.html,让它自己去玩路由匹配。
后端打包也很简单,用Maven执行mvn clean package -DskipTests,生成的Jar包用nohup java -jar campus-0.0.1-SNAPSHOT.jar > app.log 2>&1 &启动即可。建议在启动命令里加--spring.profiles.active=prod切换生产环境配置,数据库连接、文件上传路径都从application-prod.yml里读取,避免把开发环境的配置暴露在服务器上。
6.2 答辩前必须准备的三类提问
技术答辩环节,老师往往不关心功能多花哨,更看重你是否真正理解自己做的系统。围绕这个题目,我总结出三个高频提问方向和一个加分技巧。
第一类是架构问题:"为什么选择前后端分离?有什么好处?"标准回答思路是:前端关注展示和交互,后端专注业务逻辑和数据处理,两者并行开发效率高,部署时可独立扩展,前端页面通过API接口获取数据,降低耦合。最后补一句"如果后续要适配小程序或移动端,只需要复用后端接口即可",这个回答既完整又有前瞻性。
第二类是安全问题:"密码怎么存储的?如何防止SQL注入?"注意两个点:密码必须用BCrypt加密存储,不能明文;SQL操作尽量用MyBatis Plus的条件构造器,它底层是预编译的PreparedStatement,能有效防止SQL注入。如果老师在,你再加一句"文件上传也做了类型校验,只允许图片扩展名",就体现了安全意识。
第三类是优化问题:"报名人数很多时怎么保证不超人数?"把前面提到的数据库行锁方案完整讲一遍,从问题、原理到解决方案,这是一个完整的闭环回答。
除了回答问题,答辩开场的两分钟项目演示也很关键。建议演示时准备一份造好的数据:首页活动列表有分类筛选、有搜索、有分页;模拟学生报名一个活动,再模拟社团负责人发布一个新活动,切到管理员账号审核通过,刷新首页能看到新活动出现。这条完整链路演示完,比对着PPT念十分钟有效得多。
6.3 给后续想扩展的同学几个方向
如果时间和精力允许,这个系统有几个锦上添花的扩展方向。第一个是活动签到:在活动详情页生成二维码,管理员用微信扫码后输入活动码完成签到,签到记录可以在后台按活动导出Excel表格。第二个是数据可视化:用ECharts在管理后台画一张"近六个月活动数量趋势图"和"活动分类占比饼图",数据直接从报名表和活动表聚合查询,图表比表格更能打动答辩老师。第三个是消息通知:当活动审核通过或驳回时,给发起人发送站内消息提醒,用WebSocket实现实时推送,前端个人中心的消息角标实时更新。这三个方向难度依次递增,但都是在这个项目骨架上自然生长出来的,不会显得刻意堆功能。
我个人带毕设这几年最大的体会是:校园活动管理系统这类题目,赢在完成度而不是复杂度。把基础功能做扎实,每一个环节都能讲清楚为什么这样做,数据流和状态流都经得起推敲,答辩时再展示一两个自己独立实现的亮点,就是一份很稳的毕业答卷。希望上面的这些设计思路和踩坑经验能帮你少走几步弯路。