校园活动发布系统这个题目,每年毕业季都能在选题表里刷到好几遍。我第一次帮人看这类项目是几年前,一个学弟把源码和开题报告一起发过来,我打开数据库一看,活动表八个字段,报名表三个字段,报名接口先 insert 再 count 一下,连个唯一索引都没加。我问他:同一个账号连点两下报名按钮会出几条记录?他愣了几秒没答上来。这个场景基本就是这类项目的分水岭——能跑起来和能扛住问,中间差的是对并发控制、状态流转、权限边界的真实理解。这篇就把一套校园活动发布系统从需求边界、表结构、防超卖、审核流,一直说到开题报告怎么写、答辩怎么不被问倒,全部摊开讲。后端我按 Spring Boot + Vue 这套最常见的组合给示例,换成 Django 或者小程序云开发,骨架是一回事。适合正在做毕设的同学,也适合刚接手校园信息化小项目的开发者拿去做参照。
1. 项目整体定位与设计思路拆解
1.1 需求边界:先划清楚不做什么,比做什么更重要
校园活动发布系统听着简单,实际拆开会发现它是个“多方参与 + 有审核流 + 有名额竞争”的小型业务系统。角色至少三类:活动组织者(社团负责人、院系学生工作干事)、普通学生、审核方(团委或学生工作部门)。三类人看到的东西、能点的按钮、能改的字段完全不一样,这是系统复杂度的第一层来源。
核心功能我一般归纳成七块:活动创建与草稿保存、活动审核与驳回重提、活动列表与详情展示、在线报名与取消、名额控制与候补、现场签到核销、数据统计与导出。这七块是必做的,缺一块答辩时都容易被追问。
“不做什么”同样要写进开题报告,这是体现你思考深度的地方。我的建议是明确排除这几项:在线支付(校园活动绝大多数免费或线下缴费,接支付会引入一堆资质和资金问题)、站内即时聊天(用通知和评论区足够,做 IM 是给自己挖坑)、第三方账号体系对接(除非学校有统一认证,否则自己做账号密码最稳)、复杂的推荐算法(数据量根本撑不起来,硬写反而是减分项)。把这些排除理由写清楚,评审老师会觉得你是有判断力的,而不是什么火堆什么。
还有一个容易被忽略的边界:活动名额的候补机制到底做不做。做,能加分,能讲出排队算法的故事;不做,也没问题,但要在文档里说明“当前版本采用满员即止策略,候补队列作为后续迭代方向”。最怕的是文档里写了候补、代码里没有,答辩时被翻出来就很被动。
1.2 技术选型的账要算清楚,别为了炫技
我见过太多项目栽在选型上——不是选错了框架,是选了讲不明白的框架。答辩老师问“你为什么用这个”,你答“网上教程多”,这关就过不去。下面这张对比表是我自己带人做项目时常用的决策依据,直接抄作业就行:
| 技术路线 | 上手成本 | 适合场景 | 答辩友好度 | 主要风险 |
|---|---|---|---|---|
| Spring Boot + MyBatis-Plus + Vue | 中 | 功能完整、需要讲分层架构 | 高,教材和资料最多 | 环境配置容易出问题 |
| Django + 模板引擎 | 低 | 逻辑简单、想快速出效果 | 中,容易被问“是不是套模板” | 前端观感偏弱 |
| 小程序 + 云开发 | 低 | 移动端体验好、演示有亮点 | 中高,扫码演示很加分 | 后端逻辑讲不深 |
| Node.js + Express + React | 中高 | 想体现前后端分离 | 中,老师可能不熟 | 生态细节容易被追问 |
我的实际建议是:如果你的核心目标是稳过,选 Spring Boot + Vue。原因很实际:这套组合的分层结构天然对应答辩要求的“三层架构”,Controller 收参数、Service 写业务、Mapper 访问数据库,你讲起来顺畅,老师听起来熟悉。而且这套组合的坑都是被踩烂的坑,网上能搜到答案,不会在深夜两点卡在一个没人遇到过的问题上。
选 MyBatis-Plus 而不是原生 MyBatis,理由是单表 CRUD 的模板代码能省掉一大半。活动列表、报名记录这些全是最典型的单表分页查询,用 MyBatis-Plus 的Page对象三行代码搞定,省下来的时间拿去做防超卖和权限拦截,性价比高得多。至于 Vue 用 2 还是 3,看你手里的组件库——Element UI 配套 Vue2,Element Plus 配套 Vue3,别混用,混用是新手最容易踩的头号大坑。
提示:选型方案一旦写进开题报告,中途别改。改技术栈意味着推倒重来,进度表上会立刻穿帮。
1.3 权限模型:别在 Controller 里写一堆 if
新手最容易写的权限判断是这样的:在接口开头写if (user.getRole().equals("admin")),然后在另一个接口再抄一遍。业务一复杂,这类判断会散落在几十个地方,改一处漏三处。
正确的做法是引入简化的 RBAC 模型,四张表搞定:用户表、角色表、用户角色关联表、权限点表(也可以简化成角色枚举)。这套模型的价值在于,权限判断收敛到一个拦截器里,业务代码只关心“这件事该不该做”,不关心“你是谁”。
具体到本系统,我一般定义四个角色编码:STUDENT(学生,可浏览、报名、取消、评价)、ORGANIZER(组织者,可创建活动、查看自己活动的报名名单、发起签到)、AUDITOR(审核员,可审核、驳回、强制下架)、ADMIN(管理员,全部权限 + 用户管理)。
实现上用 Spring 的拦截器配合自定义注解,比如在方法上打@RequireRole("ORGANIZER"),拦截器解析注解后比对当前登录用户的角色集合。这么做的第二个好处是,前端菜单渲染可以直接复用一个权限接口,后端返回当前用户的可访问菜单树,前端动态生成路由,不用把菜单写死在代码里。
有个细节容易被漏掉:组织者能不能给别人的活动报名?能,但要单独判断,因为他同时具备学生身份。这就是角色不是互斥的、一个人可能同时是 ORGANIZER 和 STUDENT 带来的麻烦。我的处理方式是角色表允许一个用户挂多个角色,判断权限时取并集,而不是取第一个匹配。这一点如果能在开题报告的技术路线里提一句,是很实在的加分项。
2. 数据库设计:整套系统的地基
2.1 核心表清单与关键字段说明
表结构设计是这个项目里最能体现功底的部分。我把常用的九张表列出来,字段只列关键项,通用字段(创建时间、更新时间、逻辑删除标记)就不重复写了:
| 表名 | 作用 | 关键字段 |
|---|---|---|
sys_user | 用户主表 | id、账号、密码哈希、昵称、头像、学院、学号、状态 |
sys_role | 角色表 | id、角色编码、角色名称 |
sys_user_role | 用户角色关联 | 用户id、角色id(联合唯一) |
activity | 活动主表 | id、标题、封面、分类id、主办方id、开始/结束时间、报名截止时间、名额、已报名数、状态、审核意见 |
activity_signup | 报名记录 | id、活动id、用户id、报名状态、报名时间、取消时间、签到状态 |
activity_category | 活动分类 | id、名称、排序、图标 |
checkin_record | 签到记录 | id、活动id、用户id、核销时间、核销人id、核销方式 |
notification | 站内通知 | id、接收人id、标题、内容、已读标记、关联活动id |
audit_log | 审核流水 | id、活动id、审核人id、审核动作、审核意见、时间 |
字段设计上有几个点必须说清楚,因为它们直接对应答辩问题。
第一,activity表里为什么要存已报名数这个冗余字段? 按范式来说,这个数应该从报名表 count 出来,不该存。但实际场景里活动列表页一次要查二十条活动,每条都去 count 一次报名表就是二十次聚合查询,页面直接卡住。存冗余字段是典型的用空间换时间,代价是必须保证这个数和真实报名记录一致——这就引出下一节的并发问题。开题报告里把“反范式设计及其一致性保障”写一段,是很能体现水平的内容。
第二,activity_signup表为什么必须有联合唯一索引? 建一个(activity_id, user_id)的唯一索引,这是防重复报名的最后一道防线,也是最便宜的一道。应用层做判断会被并发穿透,数据库唯一索引不会。我曾经见过一个项目,应用层判断写得漂漂亮亮,就是没加索引,压测时同一个用户出现了六条报名记录,答辩演示当场翻车。
第三,时间字段用什么类型? 用datetime,不要用字符串,也不要用时间戳。用字符串会导致所有时间比较全靠代码,数据库层面没法做索引范围的筛选。报名截止、活动开始、活动结束这三个时间的大小关系,应该在建表时就写进业务约束文档里:报名截止 ≤ 活动开始 < 活动结束。
2.2 状态机:让所有流转都合法
活动状态是整个系统里最乱的地方。如果不把状态定义清楚,最后会出现“已取消的活动还能报名”“已结束的活动被驳回”这种荒唐逻辑。我一般定六个状态:
| 状态码 | 状态名 | 可执行操作 | 可流转到 |
|---|---|---|---|
| 0 | 草稿 | 编辑、删除、提交审核 | 1 |
| 1 | 待审核 | 撤回 | 0、2、3 |
| 2 | 已发布 | 查看报名、签到、取消活动 | 4、5 |
| 3 | 已驳回 | 修改后重提、删除 | 1 |
| 4 | 已结束 | 查看统计、导出 | 无 |
| 5 | 已取消 | 查看原因 | 无 |
这张表的价值在于:任何状态变更都必须在代码里显式判断来源状态是否合法。比如“提交审核”这个接口,第一行就要判断当前状态是不是 0 或者 3,不是就直接返回错误。我习惯把这种判断抽成一个枚举类,把合法的流转关系写成静态 Map,判断时查表,这样新增状态时只需要改一处。
报名记录也需要状态机,但更简单:已报名(1)、已取消(2)、已签到(3)。取消报名不是删除记录,而是改状态,因为你需要保留“谁报过名又取消了”的完整痕迹,运营统计的时候要看这个数据。这个设计选择一定要在文档里写明白,很多人图省事直接 delete,被问到“用户取消报名后还能不能再报”就答不上来——用状态机的话,答案是“把状态改回 1,但受名额和截止时间限制”,逻辑自洽。
注意:状态流转的接口一定要做幂等。用户连点两次“提交审核”,第一次成功把状态从 0 改成 1,第二次进来发现当前状态是 1,直接返回“请勿重复提交”。没有这个判断,审核队列里会出现两条一模一样的待审任务。
2.3 索引与查询优化:先想清楚页面要查什么
索引不是拍脑袋加的,是从页面查询倒推的。我把主要页面的查询条件列一遍:
- 学生端活动列表:按分类筛选、按状态(已发布)筛选、按时间排序、关键词模糊搜索、分页
- 我的报名:按用户id查、按报名状态筛选、按活动开始时间排序
- 组织者后台:按主办方id查、按活动状态筛选
- 审核工作台:按状态=1查、按提交时间排序
- 签到页:按活动id查、按学号/姓名模糊搜索
对应的索引方案:
-- 活动列表主查询 ALTER TABLE activity ADD INDEX idx_status_start (status, start_time); ALTER TABLE activity ADD INDEX idx_category_status (category_id, status); ALTER TABLE activity ADD INDEX idx_organizer (organizer_id, status); -- 报名表:唯一索引是基础,再加两个查询索引 ALTER TABLE activity_signup ADD UNIQUE KEY uk_activity_user (activity_id, user_id); ALTER TABLE activity_signup ADD INDEX idx_user_status (user_id, signup_status); -- 审核工作台 ALTER TABLE activity ADD INDEX idx_status_submit (status, submit_time);模糊搜索这一块我要多说一句。很多人直接在标题字段上加普通索引然后LIKE '%关键词%',索引完全用不上。数据量小的时候没感觉,数据量一上来就露馅。校园系统的数据量通常撑死几千条活动,用不上索引也不会慢到哪去,所以这里不要过度设计,老老实实LIKE就行,但要在文档里说明“当前数据规模下采用模糊匹配,后续可引入全文索引”。这种“知道问题在哪但判断现阶段不需要解决”的表述,比硬上一堆看不懂的优化方案更能体现判断力。
3. 后端核心接口实现与并发控制
3.1 活动发布接口:校验链路要一层不漏
发布接口是脏数据的主要来源,校验必须做全。我把校验按照“参数格式 → 业务规则 → 权限 → 数据一致性”四层来组织,每一层失败返回的错误码都不一样,方便前端定位。
@PostMapping("/activity/publish") @RequireRole("ORGANIZER") public Result publish(@RequestBody @Valid ActivityDTO dto) { // 第一层:Bean Validation 注解已完成非空、长度、格式校验 // 第二层:业务时间规则 LocalDateTime now = LocalDateTime.now(); if (dto.getSignupDeadline().isAfter(dto.getStartTime())) { return Result.fail("报名截止时间不能晚于活动开始时间"); } if (!dto.getStartTime().isBefore(dto.getEndTime())) { return Result.fail("活动结束时间必须晚于开始时间"); } if (dto.getSignupDeadline().isBefore(now)) { return Result.fail("报名截止时间不能早于当前时间"); } // 第三层:名额合理性 if (dto.getQuota() == null || dto.getQuota() < 1 || dto.getQuota() > 2000) { return Result.fail("活动名额需在 1 到 2000 之间"); } // 第四层:同一组织者短时间内不允许重复发布同名活动 long dup = activityMapper.countRecentSameTitle( currentUserId(), dto.getTitle(), now.minusMinutes(10)); if (dup > 0) { return Result.fail("请勿重复提交相同活动"); } Activity entity = ActivityConverter.toEntity(dto, currentUserId()); entity.setStatus(ActivityStatus.DRAFT.getCode()); activityMapper.insert(entity); return Result.ok(entity.getId()); }这里有个细节值得讲:为什么要有第四层的重复提交判断。用户网络卡顿的时候会连点两下发布按钮,第一次请求成功,第二次也进来了,结果数据库里出现两条一模一样的活动。前端加防抖能挡掉大部分,但挡不住网络层重发。后端这道判断是兜底,成本极低,收益很高。
另外说个实操心得:内容字段的长度限制一定要在数据库和代码里保持一致。我在一个项目里遇到过,前端限制 2000 字,后端 DTO 没加@Size,数据库字段是varchar(2000),用户粘贴了一段 2500 字的文案,直接抛出数据库截断异常,前端显示的是一个看不懂的 500 错误。加一个@Size(max = 2000)就解决了,但用户看到的是“活动描述不能超过 2000 字”这种能理解的提示。这种事不大,但特别影响体验。
3.2 报名名额扣减:这是整个项目唯一的真并发点
我敢说,校园活动发布系统答辩时被问到的技术问题,有七成集中在这里。场景很具体:活动名额剩最后一个,两个学生同时点报名,怎么保证不超卖?
先说错误的写法,因为大部分人第一反应就是这个:
// 错误示范:先查后改,中间有窗口期 Activity act = activityMapper.selectById(id); if (act.getSignedCount() >= act.getQuota()) { return Result.fail("名额已满"); } activityMapper.increaseSignedCount(id); // 两个线程都执行了这行 signupMapper.insert(record);问题在于“查”和“改”之间隔着几毫秒,两个线程都查到signedCount = quota - 1,都判断可以报名,然后都执行了加一,最终signedCount比quota多了 1,超卖了。
我推荐三种方案,按项目实际情况选:
方案一:数据库乐观锁(推荐,最省事)
给activity表加一个version字段。更新时带上版本号做条件:
UPDATE activity SET signed_count = signed_count + 1, version = version + 1 WHERE id = #{id} AND version = #{version} AND signed_count < quota;代码里判断影响行数,是 1 就成功,是 0 就说明要么名额满了,要么被别人抢先改了版本,让用户重试。这种方案的优点是不需要额外组件,一个字段一行 SQL 搞定,答辩讲起来也清楚。缺点是高并发下重试率会上升,但校园场景一天几百人报名,完全够用。
其实这里可以直接把version省掉,只用signed_count < quota做条件,因为这条 UPDATE 在数据库层面是原子操作,行锁会串行化这些更新。加了 version 是为了在业务上区分“名额满”和“更新冲突”两种失败原因,报错更精准。
方案二:Redis 预扣减(数据量大的时候讲这个加分)
活动发布时把名额同步到 Redis,报名时用 Lua 脚本做“判断 + 扣减”的原子操作,扣减成功再异步落库。这个方案性能好,但引入了一致性问题——Redis 扣了,数据库写失败了怎么办?需要补偿机制。我的建议是:如果你没把握讲清楚补偿逻辑,别用这个方案,答辩老师一句“Redis 扣了但数据库没写进去怎么办”就能问穿。
方案三:报名表唯一索引兜底
不管用哪种方案,唯一索引都必须加。它是最后一道防线:即使前面的逻辑全错了,数据库也不会让同一个用户在一个活动里有两条有效记录。这个点一定要在答辩时主动说,它体现的是“我知道应用层不可靠,所以我在数据层做了兜底”的防御性思维。
还有个真实场景要说:取消报名后名额怎么处理。取消报名是把signup_status改成 2,同时把signed_count减一。这里有个坑:如果用户 A 报名、取消、再报名,循环十次,signed_count会不会算错?会,如果减一操作没有幂等保护。我的处理是减一的时候也带上条件判断,确保只对“当前状态为已报名”的记录做减一:
UPDATE activity_signup SET signup_status = 2 WHERE id = #{id} AND signup_status = 1; -- 上面的语句影响行数为 1 才执行下面这句 UPDATE activity SET signed_count = signed_count - 1 WHERE id = #{activityId} AND signed_count > 0;两条语句放在同一个事务里,要么都成,要么都不成。
3.3 签到核销:二维码怎么做才安全又简单
签到这块很多人做复杂了。我的思路是:组织者打开签到页,页面展示一个二维码,学生扫码后跳转到核销页,输入学号后四位确认,完成签到。核销逻辑要幂等,重复扫码只能成功一次。
二维码内容不要直接放活动 id 明文,否则别人随便改个数字就能签到别的活动。我的做法是放一个带签名的令牌:
// 生成:活动id + 时间戳 + 签名 String raw = activityId + ":" + expireAt; String sign = DigestUtils.md5DigestAsHex( (raw + SECRET_SALT).getBytes(StandardCharsets.UTF_8)); String token = Base64.getUrlEncoder().encodeToString((raw + ":" + sign).getBytes());核销接口收到 token 后先验签,再判断是否过期,然后查这个用户名下有没有该活动的有效报名记录,有就把签到记录写进去,写之前先查checkin_record表里有没有同样的(activity_id, user_id)记录,有就直接返回“已签到”。
用 MD5 做签名在校园项目里足够了,它的作用是防篡改,不是防破解。开题报告里可以说明“采用哈希签名保证令牌不可伪造,密钥存放于服务端配置”,这个表述既准确又不会引来深挖。
提示:二维码有效期别设太长,按活动时长加两小时足够。设置成永久有效的令牌,活动结束后还在外面流传,是安全隐患。
3.4 定时任务与通知:让系统自己动起来
三个定时任务基本是标配:
- 活动自动结束:每小时扫一遍,把
结束时间 < 当前时间且状态为 2 的活动改成 4。 - 报名截止提醒:每天扫一遍,把 24 小时内截止报名的活动推送给还没报名的学生(用户量大的话要分批,别一次全推)。
- 活动开始前提醒:给已报名的用户推送“活动两小时后开始”。
@Scheduled(cron = "0 0 * * * ?") public void autoCloseActivity() { List<Activity> list = activityMapper.selectList( new LambdaQueryWrapper<Activity>() .eq(Activity::getStatus, 2) .lt(Activity::getEndTime, LocalDateTime.now())); for (Activity act : list) { act.setStatus(4); activityMapper.updateById(act); } log.info("自动结束活动 {} 条", list.size()); }这里有个坑我必须提:定时任务和用户操作之间会打架。用户正在报名一个活动,定时任务刚好把活动状态改成已结束,报名的判断逻辑如果先查状态后面又有几毫秒延迟,就可能出现“报名成功但活动已结束”的脏数据。解决办法是报名接口里在事务内重新查一次活动状态,用SELECT ... FOR UPDATE锁住那一行。这个细节说出来,答辩老师一般会点头。
通知功能我建议就用站内信,别接短信。原因很实际:短信要钱、要审核、要配模板,而且校园项目的通知量完全不需要实时触达。站内信一张表搞定,前端顶栏加个红点,够用了。
4. 前端页面与后台管理的关键实现
4.1 学生端:列表页的体验决定第一印象
学生端只有三个页面是真正重要的:活动列表、活动详情、我的报名。列表页的体验好坏,直接决定演示时的观感。
列表页我做这几件事:卡片式布局展示封面、标题、时间、地点、名额进度条;顶部固定分类标签横向滚动;支持按时间/热度排序;下拉到底自动加载下一页。名额进度条这个细节很值得做,用不同颜色区分“充足 / 紧张 / 已满”,用户一眼就知道要不要赶紧报。
详情页的关键是按钮状态要准。同一个报名按钮,根据不同情况要显示不同文案和状态:
| 场景 | 按钮文案 | 是否可点 |
|---|---|---|
| 未登录 | 登录后报名 | 可点,跳登录 |
| 可报名 | 立即报名 | 可点 |
| 名额已满 | 名额已满 | 不可点 |
| 已过截止时间 | 报名已截止 | 不可点 |
| 已报名 | 取消报名 | 可点 |
| 活动已结束 | 活动已结束 | 不可点 |
| 自己创建的活动 | 管理活动 | 可点,跳后台 |
这张表建议直接抄进代码,用一个计算属性根据后端返回的字段统一算出来。最忌讳的是在模板里写一堆v-if嵌套,改一处忘一处,最后出现“已报名的活动还显示立即报名”这种低级问题。
我的报名页要做的事更简单:分“待参加 / 已结束 / 已取消”三个 tab,每张卡片下面放“取消报名”和“查看详情”两个按钮,取消要二次确认。二次确认这一步别省,误触取消再想报名可能名额就没了,用户体验会很差。
4.2 管理端:审核工作台是演示的重头戏
后台管理页面的数量比学生端多,但绝大多数是标准 CRUD,用组件库的表格 + 弹窗表单就能拼出来。真正需要设计的是审核工作台。
审核工作台的核心诉求是“快”。一个审核员一天可能要处理几十条申请,所以要做到列表直接展示关键信息(标题、主办方、时间、名额、封面缩略图),点一下展开详情抽屉,抽屉里直接有“通过 / 驳回”两个按钮,驳回必须填意见。整个流程不跳页,处理完自动定位到下一条。
数据看板也是演示时的加分项。我一般放四个卡片加两个图:活动总数、报名总人次、本月新增活动、待审核数量,再加一个近七天报名趋势折线图和一个分类占比饼图。数据用后端一个统计接口一次返回,别搞四个接口让前端并发请求,简单点。
导出功能用 EasyExcel 之类的库实现,导出报名名单的时候注意一件事:手机号、学号这类字段要做脱敏。导出成 Excel 直接给组织者,里面全是完整手机号,这在数据安全上是有问题的。我在项目里的做法是默认导出脱敏版本,需要完整信息时由管理员单独申请。这个设计点在开题报告的“系统安全设计”一节里写上,是很实用的内容。
5. 开题报告怎么写才不被卡
5.1 必备结构与本系统的内容映射
开题报告被退回,八成不是技术不行,是结构不完整或者内容太空。标准的开题报告至少包含这几块,我按本系统给出对应内容:
| 章节 | 要写什么 | 本系统怎么写 |
|---|---|---|
| 选题背景与意义 | 为什么做这个 | 校园活动信息分散在群聊和海报里,报名靠接龙,统计靠手工,效率低且易出错 |
| 国内外研究现状 | 别人做到什么程度 | 综述现有活动管理类系统的功能特点,指出通用系统对校园场景适配不足 |
| 研究内容 | 你要做什么 | 列出七大功能模块,每个模块两三句话说明 |
| 技术路线 | 用什么做、怎么做 | 后端分层架构、前端组件化、数据库设计、关键算法说明 |
| 重点与难点 | 你解决了什么难题 | 名额并发控制、审核状态机、权限模型 |
| 进度安排 | 时间怎么分配 | 按周排,见下表 |
| 参考文献 | 支撑材料 | 不少于 10 篇,含期刊、书籍、技术文档 |
进度安排这张表我建议排得细一点,显得你真有计划:
| 阶段 | 时间占比 | 主要工作 | 交付物 |
|---|---|---|---|
| 需求分析 | 15% | 调研、功能梳理、用例图 | 需求规格说明 |
| 系统设计 | 20% | 架构设计、数据库设计、接口设计 | 设计文档、ER 图 |
| 编码实现 | 40% | 分模块开发、联调 | 可运行源码 |
| 测试优化 | 15% | 功能测试、并发测试、缺陷修复 | 测试用例与报告 |
| 文档撰写 | 10% | 论文撰写、修改、答辩准备 | 论文终稿、演示材料 |
“重点与难点”这一节是最容易写出差异化的地方。别写“界面美观”“功能完整”这种谁都能写的话,就写具体的:高并发下的名额一致性保障、多角色权限的细粒度控制、活动状态流转的合法性约束。每个难点下面写清楚问题是什么、你用了什么方案、为什么选这个方案,三段话,评审看了就知道你真做过。
5.2 容易被问倒的几个点,提前准备好答案
我把带过的项目里被问到的频率最高的问题列出来,每个都给一个能站住的答法。
问:你这个系统的创新点在哪?
这题千万别硬编创新。校园活动管理系统是个成熟品类,硬说自己首创反而露怯。我的答法是从“场景适配”切入:通用活动平台面向社会人群,缺少校园特有的审核流和院系维度;本系统把审核流程和院系归属做成了内建能力,同时针对校园活动的免费属性去掉了支付模块,整体更轻。这个答法诚实、有逻辑,比编一个“基于大数据智能推荐”要强得多。
问:如果同时有一万人报名,你的系统撑得住吗?
先承认边界,再讲方案。答法:当前部署环境下没有做过万人级压测,但针对名额竞争这个核心场景做了乐观锁和唯一索引双重保障,理论上不会超卖;如果真要支撑这个量级,需要引入缓存和消息队列做削峰,这属于后续优化方向。承认边界不丢人,硬吹才丢人。
问:你的数据库设计为什么这样分表,能不能合并?
这题考的是你的设计理由。答:用户、活动、报名三者是一对多和多对多关系,合并会导致大量冗余和更新异常;报名表独立出来还承担了保留历史记录的作用,取消报名用状态标记而不是删除,便于统计。讲清楚“为什么这样”比“这样是什么”重要得多。
问:代码是你自己写的吗?
被问到这句说明前面某个环节露了怯。应对方式是当场打开 IDE,让老师随便点一个模块,你讲这段代码的思路。这个准备一定要提前做:挑三个模块,把里面的每一行都能讲清楚,最好是防超卖、权限拦截、状态机这三块,因为它们最有内容。
6. 常见问题排查与实操避坑实录
6.1 开发期高频问题速查表
下面这张表是我这些年攒下来的,基本覆盖了这个项目 80% 的报错场景:
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
| 前端请求全部 404 | 代理配置的 target 少了端口,或后端 context-path 不一致 | 打开浏览器网络面板看真实请求地址,与后端启动日志里的端口逐字比对 |
| 跨域报错 CORS | 后端没配全局跨域,或配了但拦截器在跨域之前执行 | 用全局配置类统一处理,别在单个 Controller 上打注解 |
| 中文乱码 | 数据库连接串没加字符集,或建表时用了 latin1 | 连接串加useUnicode=true&characterEncoding=utf8,表统一 utf8mb4 |
| 时间差 8 小时 | 数据库时区、JVM 时区、序列化格式三者不一致 | 连接串指定serverTimezone=Asia/Shanghai,实体类统一用 LocalDateTime |
| 分页总数不对 | 联合查询时 count 语句没去重 | 检查 count 是否用了distinct,或改用两次查询 |
| 上传图片后访问 404 | 静态资源映射路径和保存路径不一致 | 映射配置里把本地目录映射到访问前缀,路径用绝对路径 |
| 定时任务不执行 | 启动类没加@EnableScheduling | 加注解,同时确认 cron 表达式是六位还是七位 |
| 报名数比实际多 | 缺唯一索引,或减一逻辑没做幂等 | 补索引,减一语句加状态条件 |
6.2 部署环节的坑,比写代码还磨人
本地跑通不等于能部署,这是我踩过最多次的地方。
第一个坑是数据库初始化脚本不完整。本地开发时表是手动建的,导出脚本时漏了索引和外键,换台机器一跑就报错。解决办法是从一开始就用脚本管理表结构,每次改表都同步更新脚本,并且在干净的环境里完整跑一遍验证。我习惯在脚本开头加DROP TABLE IF EXISTS,保证可以重复执行。
第二个坑是配置文件里的密码和密钥进了代码仓库。数据库密码、签名盐值这些绝对不能写死在代码里提交上去。正确做法是放配置文件并且把配置文件加进忽略清单,提供一个application-example.yml作为模板,让别人填自己的。这个习惯从学生项目就该养成。
第三个坑是前端打包后的路由模式。开发时用 hash 模式一切正常,打包部署到服务器后如果用了 history 模式又没配服务端重定向,刷新页面直接 404。要么老实用 hash 模式,要么在 nginx 配置里加try_files $uri $uri/ /index.html。
第四个坑是上传目录权限。部署到服务器后图片上传失败,查半天发现是运行用户对目标目录没有写权限。这类问题排查思路很固定:看日志报什么错、看目录权限、看磁盘空间,三分钟能定位。
6.3 答辩演示的保命技巧
演示环节出问题,比代码有 bug 更致命,因为只有一次机会。
提前录屏是底线。把完整流程走一遍录下来,万一现场网络或环境出问题,直接放录屏,边放边讲。这不是作弊,是专业。
演示数据要提前铺。空数据库里点来点去什么都看不出来。提前准备:二十条不同状态的活动、三条待审核的申请、十条报名记录、一个已经结束可以导出名单的活动。演示时从“审核一条申请”开始,到“学生报名”“组织者签到”“导出名单”结束,形成一个完整闭环,五分钟讲完,干净利落。
别现场改代码。有人喜欢现场演示改一个 bug 展示能力,风险极高。把自己的代码讲明白就够了。
准备一个能跑的降级方案。如果服务器连不上,本地有备份环境能起,或者直接切录屏。三套方案备着,心里就不慌。
我在几个项目里反复验证过一件事:这类系统的分数差异,从来不在功能多少,而在你对“为什么这么做”的表述是否清晰。同样是防超卖,有人只会说“我加了锁”,有人能说清楚“应用层判断会被并发穿透,所以我在数据库层用条件更新兜底,再加唯一索引做最后防线”——这两种回答在评审耳朵里完全是两个层次。代码写完只是开始,把每个设计决策背后的判断讲出来,才是这套东西真正的价值所在。