项目标题是“校园活动发布系统源码开题”。这类题目每年都有很多人在做但真正能讲到关键处的很少。我接触过不少做这类项目的同学也帮人看过很多前后端代码最常见的问题在于“系统跑起来了但说不清楚为什么这么设计”。下面把我对这套系统的理解、落地的思路以及踩过的坑完整写一遍。1. 先把需求想透校园活动发布系统到底卡在哪1.1 从“贴海报”到“发活动”的迁移逻辑很多人一上来就问“用什么框架”其实这属于跳过需求直接讨论实现。校园活动发布系统本质上是把线下那套“海报墙、报名表、签到表”搬到线上但这个迁移不是简单搬运中间会冒出很多线下不存在的问题。线下贴海报信息发出去了就完事线上发活动发出去只是开始后面还有名额、审核、签到、取消、统计一长串流程。我理解的这套系统核心是把“活动的生命周期”用数据管起来。一个活动从草稿到结束中间要经过提交、审核、开放报名、报名截止、进行中、已结束、已取消这几步每一步对应不同的操作权限和不同的数据可见性。如果只做了“增删改查”四件事系统在答辩时看起来功能不少但只要被问到“活动取消之后已经报名的人怎么办”就答不上来了。所以第一步不是画页面是画一张状态流转图用笔在纸上把每个状态、谁能触发、触发后哪些字段变化写清楚。这张图画明白了后面建表、写接口、做页面都是顺着来的效率会高出一大截。提示需求阶段多花两小时编码阶段能省两天。尤其是状态流转和权限边界这两个想不清楚代码写到一半一定要返工。1.2 三类使用者的真实诉求拆解系统里至少有三类角色他们的关注点完全不同如果用一个角色把所有功能都塞进去做出来会非常难受。学生侧关注的是“快速找到我能参加的活动”和“我报了哪些、什么时候签到”。他们对信息的筛选要求很高按分类、按时间、按是否还有名额、按是否加志愿时长。学生还有一个隐藏诉求——活动的可信度如果一个活动没有组织方信息、没有审核标识报名意愿会很低。组织者侧社团、班级、院系的学生干部关注的是“发布效率”和“报名管理”。他们最烦的是重复劳动发一个活动要填一堆字段报名截止后要手动导出名单一个个核对。所以模板化发布、一键导出名单、批量审核报名这些才是他们真正用得上的功能。管理员侧关注的是“审核”和“冲突检测”。同一时间段、同一场地被两个活动同时占用这是真实会发生的事也是答辩时很能加分的一个点。管理员还需要数据看板本月活动数、报名人次、各分类占比这些数据在后台用几条聚合查询就能出来。把这三类人的诉求分别列成清单再去决定哪些做、哪些不做比对着“功能大全”抄一遍靠谱得多。1.3 为什么我不建议一上来就堆功能我见过不少项目功能列表写了二十多条实际点进去一半是空的或者只是弹一个“功能开发中”。这在答辩现场是扣分项因为老师会挑着问问到一个没实现的就很尴尬。做减法比做加法难。我的建议是核心闭环一定要完整也就是“发布 → 审核 → 报名 → 签到 → 统计”这条线每个环节都要能真正跑通有数据、有反馈、有异常处理。外围功能比如评论、点赞、活动收藏、消息推送可以作为可选模块做一两个就行但不要为了凑数量做一堆半成品。还有一个现实问题功能越多表越多联调成本越高数据库设计的复杂度也越高。一个人做毕设的时间是有限的把有限的时间投入到“让主流程顺滑”收益远大于“什么都做一点”。2. 技术选型与数据库设计不追新追“能跑起来还能讲清楚”2.1 前后端技术栈的取舍技术栈的选择有一个很现实的判断标准你能否在十分钟内解释清楚它的请求链路。答辩时老师问“用户点报名这个按钮之后发生了什么”你要能从前端事件讲到接口、到服务层、到数据库、再讲回响应这条链讲不断就说明你真的懂。主流的几条路线Java 系的 Spring Boot MyBatis-Plus MySQL Vue生态成熟、参考资料多、招聘市场上也用得多Python 系的 Django/Flask Vue开发速度快适合时间紧的情况PHP 的 ThinkPHP Layui上手最快但前后端分离做得不够彻底讲起来容易被追问“为什么还混着模板渲染”。我倾向于 Spring Boot Vue3 Element Plus 这套原因不复杂分层清晰Controller、Service、Mapper 三层职责分明写的时候不容易把业务逻辑堆到接口里加上 Vue 的组件化页面复用方便做完活动列表页做报名记录页基本是改改字段的事。有一条要提醒不要为了“显得高级”去上微服务、消息队列、Redis 集群。单机部署能跑通、能演示就是好方案。真要用 Redis也建议只用在缓存活动列表或签到码这类地方并且要能说清楚“不用会怎样”。2.2 数据库表结构设计的核心思路表设计是这套系统的骨架也是答辩最容易被抠的地方。我一般按“主体、关系、记录”三类来分。主体表用户表、活动表、分类表、场地表。关系表报名表用户与活动、组织者关联表用户与可发布的活动范围。记录表签到记录、审批日志、操作日志。核心几张表的结构大致如下以 MySQL 为例字段做了精简表名作用关键字段设计要点user用户信息id、账号、密码哈希、昵称、角色、状态角色用枚举字符串不用数字可读性强activity活动主体id、标题、分类id、组织者id、开始/结束时间、报名截止时间、名额上限、已报名数、状态冗余“已报名数”字段避免每次 countsignup报名记录id、活动id、用户id、状态、报名时间、签到时间(活动id, 用户id) 建唯一索引防重复checkin签到记录id、活动id、用户id、签到方式、签到时间与 signup 分开签到现在和报名状态解耦category分类id、名称、排序、是否启用加排序字段前端才可控audit_log审批日志id、活动id、操作人、原状态、新状态、备注、时间便于回溯“谁批的、什么时候”有两个细节值得单独说。第一activity表里我特意冗余了signed_count。如果不冗余每次列表页要显示“已报名 / 总名额”就得对 signup 表做子查询或者 count活动一多页面明显卡。冗余字段的代价是要在报名和取消时同步维护这个用事务包起来就行。第二signup表的唯一索引非常关键。它不是可有可无的约束而是防重复报名的最后一道防线。业务代码里当然要先查一遍“是否已报名”但并发情况下两个请求可能同时通过检查唯一索引能兜住这种情况插入失败抛异常捕获后返回“你已报名”。这个点答辩时讲出来很能体现实战意识。CREATE TABLE signup ( id bigint NOT NULL AUTO_INCREMENT, activity_id bigint NOT NULL, user_id bigint NOT NULL, status varchar(20) NOT NULL DEFAULT PENDING, signup_time datetime NOT NULL, remark varchar(255) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_activity_user (activity_id,user_id), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字符集统一用utf8mb4不要用utf8。这个坑很多人踩过活动标题里带一个生僻字或者特殊符号插入直接报错排查半天才发现是字符集问题。2.3 开题报告怎么写才不像模板开题这部分我见过太多“研究背景”写了三段正确的废话。老师看开题不是看你文笔是看你知不知道自己在做什么。我的经验是把开题拆成四个必答问题每个问题用实打实的内容回答问题一为什么做这个不要说“信息化时代背景下”要说具体场景。比如“某学院社团每学期要办几十场活动报名靠群接龙、签到靠纸质表报名统计经常对不上取消活动后通知不到人”。场景越具体越像真的做过调研。问题二别人做到什么程度了现状分析不要罗列一堆系统名字然后说“功能单一”。可以对比几个方向通用型活动平台功能全但对校园场景不贴合比如没有班级、院系这类组织结构也没有志愿时长统计学校自建系统往往只覆盖大型活动小活动还是要走线下。找到这个缝隙你的课题定位就立住了。问题三你打算怎么做技术路线要写成流程不要写成名词堆砌。比如“先完成数据库设计并通过样例数据验证再实现用户与权限模块然后按活动生命周期实现发布与审核接着做报名与签到的并发控制最后完成统计报表与部署”。有顺序、有依赖关系看起来就像真的排过工期。问题四创新点在哪一个人做的小系统谈不上一鸣惊人但可以谈“针对性”。比如针对校园场景做了场地时段冲突检测针对志愿类活动做了时长自动累计针对组织者做了名单一键导出与批量审核。这些点小但具体、可验证比空谈“智能化”强。进度安排建议按周排留出至少两周给联调和论文修改。我见过太多人把进度排到答辩前一天结果最后一周系统还在报错论文只能凑。3. 核心功能模块的实操实现3.1 活动状态机与审核流程怎么落地状态机是这套系统的“主干”。我一般定义两个状态字段activity.status表示活动整体状态signup.status表示单个报名记录状态。两者独立不要混在一个字段里。活动状态我定义为状态值含义可执行操作DRAFT草稿编辑、提交审核、删除PENDING待审核撤回、管理员审核REJECTED已驳回编辑后重新提交APPROVED审核通过待开放开放报名、取消SIGNUP报名中关闭报名、取消、签到ONGOING进行中签到、结束FINISHED已结束查看统计CANCELED已取消查看原因状态流转必须在服务层统一收口不能散落在各个 Controller 里。我的做法是写一个ActivityStateMachine工具类把“当前状态 目标操作”映射成“是否允许”和“目标状态”所有变更都走它。public enum ActivityStatus { DRAFT, PENDING, REJECTED, APPROVED, SIGNUP, ONGOING, FINISHED, CANCELED } public class ActivityStateMachine { private static final MapActivityStatus, SetActivityStatus ALLOWED Map.of( ActivityStatus.DRAFT, Set.of(ActivityStatus.PENDING), ActivityStatus.PENDING, Set.of(ActivityStatus.APPROVED, ActivityStatus.REJECTED), ActivityStatus.REJECTED, Set.of(ActivityStatus.PENDING), ActivityStatus.APPROVED, Set.of(ActivityStatus.SIGNUP, ActivityStatus.CANCELED), ActivityStatus.SIGNUP, Set.of(ActivityStatus.ONGOING, ActivityStatus.CANCELED), ActivityStatus.ONGOING, Set.of(ActivityStatus.FINISHED) ); public static void check(ActivityStatus from, ActivityStatus to) { if (!ALLOWED.getOrDefault(from, Set.of()).contains(to)) { throw new BizException(当前状态不允许此操作 from - to); } } }这样写的好处是规则集中在一处改起来方便也方便在论文里画一张状态转移表说明每个状态之间的边。答辩老师如果问“活动能不能从待审核直接跳到已结束”你能立刻答“不能状态机里没有这条边”。审核流程本身要留痕。每次状态变更都往audit_log里插一条记录操作人、原状态、新状态、备注和时间。这个表在答辩时是加分项因为它体现了“可追溯”的设计意识而且统计页面可以直接用它算“平均审核时长”。3.2 报名名额控制并发下的防超卖这是整套系统里技术含量最高的一块也是最容易出问题的一块。名额控制的本质是“一个有限资源被并发扣减”如果不加控制很容易出现超额报名。先说错误的写法很多人第一版都是这么写的// 错误示范先查后改并发下必然超卖 Activity a activityMapper.selectById(id); if (a.getSignedCount() a.getCapacity()) { activityMapper.updateSignedCount(id, a.getSignedCount() 1); signupMapper.insert(...); }两个请求同时查到signedCount 9capacity 10都判断通过都去加一结果就是 11 个人报名成功。这个 bug 在本地单人测试时永远测不出来。正确的做法有两种我一般两个都用双保险。第一种数据库条件更新乐观锁思路UPDATE activity SET signed_count signed_count 1 WHERE id #{activityId} AND signed_count capacity AND status SIGNUP AND signup_deadline NOW();这条 SQL 执行完返回影响行数如果是 0说明名额已满或者状态不对直接回滚返回“报名失败名额已满”。因为 InnoDB 的行锁机制这条 update 本身是原子的天然防并发。第二种唯一索引兜底前面提到uk_activity_user唯一索引就是干这个的。业务上先查“是否已报名”只是为了给出友好提示真正的防重复靠数据库。插入时捕获DuplicateKeyException转成业务异常返回。完整的事务方法大致是Transactional(rollbackFor Exception.class) public void signUp(Long activityId, Long userId) { // 1. 条件更新占位失败即名额不足 int affected activityMapper.tryOccupySeat(activityId); if (affected 0) { throw new BizException(报名失败名额已满或报名已截止); } // 2. 插入报名记录唯一索引防重复 try { Signup s new Signup(); s.setActivityId(activityId); s.setUserId(userId); s.setStatus(APPROVED); s.setSignupTime(LocalDateTime.now()); signupMapper.insert(s); } catch (DuplicateKeyException e) { throw new BizException(你已经报名过该活动了); } // 3. 记录操作日志 logService.record(userId, activityId, SIGNUP); }取消报名要记得把名额还回去同样是条件更新signed_count - 1 where signed_count 0并把 signup 状态改成 CANCELED 而不是物理删除。物理删除会导致“用户到底报没报过”查不到统计也会失真。软删除配合唯一索引还有个问题取消后用户再报名会因为唯一索引冲突。解决办法是把唯一索引改成(activity_id, user_id, status)或者直接在取消时把记录状态改掉、重新报名时做 update 而不是 insert。我倾向后者逻辑更简单。注意压力测试这一步别省。用工具并发发 50 个报名请求看最终signed_count是不是刚好等于capacity。这个测试结果截图放进论文比任何文字描述都有说服力。3.3 签到核销二维码与手动码的取舍签到这块看着简单实际有不少设计选择。方案一活动生成一个统一的签到码现场展示学生扫码签到。优点是实现简单一个活动一个随机字符串存到活动的checkin_code字段。缺点是没法防止“人不在现场也让别人代签”因为码是公开的。方案二每个报名记录生成唯一码可以是报名 id 加签名也可以是随机串存在 signup 表里。学生打开自己的报名详情页看到码工作人员扫码核销。安全性高但要挨个扫人多时排队。方案三动态签到码后台定时刷新比如每 60 秒换一次页面用轮询或者 WebSocket 拿最新码。这个方案能有效防代签但实现成本高一些答辩时讲出来比较亮眼。我的建议是方案一和方案二结合活动开场用统一码快速签到结束后对未签到的人用个人码补签。这样既照顾了效率又保留了准确性。签到接口要做幂等。同一个用户重复提交签到请求不应该报错也不应该产生多条记录。处理方式是在checkin表对(activity_id, user_id)建唯一索引插入冲突时返回“已签到”前端提示“你已完成签到”。INSERT INTO checkin (activity_id, user_id, checkin_type, checkin_time) VALUES (#{activityId}, #{userId}, #{type}, NOW()) ON DUPLICATE KEY UPDATE checkin_time checkin_time;ON DUPLICATE KEY UPDATE后面跟一个“自己赋给自己”的写法就是为了“存在则忽略、不报错”配合唯一索引实现幂等。签到还有一个容易被忽略的点签到时间窗口。不能让用户在活动开始前三天就签到也不能活动结束一周后还能签。我一般在活动表里加checkin_start和checkin_end两个字段默认值是活动开始前 30 分钟到结束后 2 小时组织者可以调整。接口进来先校验时间窗口不满足直接拒绝。3.4 权限控制别把判断散落在每个接口里权限这块最忌讳的写法是在每个接口开头写if (user.getRole().equals(ADMIN))。这种做法的问题是一旦规则变化要改十几处而且容易漏。我的做法分两层。第一层是 URL 级拦截。用 Spring Security 或者自己写一个拦截器按路径前缀做粗粒度控制/api/admin/**只允许管理员/api/organizer/**允许组织者和管理员/api/student/**登录即可访问。这一层解决“这类人能不能访问这类接口”。第二层是数据级校验。这一层必须在 Service 里做因为要判断“这条数据是不是你的”。比如组织者要修改活动必须校验activity.organizerId currentUserId管理员则跳过这个校验。这个判断可以抽成一个注解加 AOP也可以写成一个工具方法但一定要收口不要每个接口各写一套。Component public class ActivityPermissionChecker { public void checkEditable(Activity a, LoginUser user) { if (ADMIN.equals(user.getRole())) return; if (!a.getOrganizerId().equals(user.getId())) { throw new BizException(无权操作他人的活动); } if (ActivityStatus.FINISHED.name().equals(a.getStatus())) { throw new BizException(已结束的活动不可编辑); } } }角色我一般只设三个STUDENT、ORGANIZER、ADMIN。不建议一上来搞七八个角色权限模型复杂了自己都记不住谁能干什么。如果确实需要细分比如“院系管理员”和“校级管理员”可以在 ADMIN 下面加一个scope字段区分数据范围而不是加角色。登录态用 JWT 比较常见token 过期时间设 2 小时左右配合 refresh token 或者干脆让用户重新登录。密码存储必须用 BCrypt 这类带盐的哈希绝对不能明文或者简单 MD5。这一点答辩必被问答“用了 BCrypt每个用户盐值不同彩虹表攻击无效”就稳了。4. 部署与演示环境答辩现场不能出意外4.1 本地打包与服务部署答辩现场最常见的翻车就是“在我电脑上是好的”。提前把部署流程走通一遍比多写两个功能重要得多。后端打包用 Mavenmvn clean package -DskipTests出一个 jar直接java -jar就能跑。这里有个坑配置文件里的数据库连接别写死成开发环境。用application-prod.yml单独放线上配置启动时指定--spring.profiles.activeprod。这样本地开发和生产部署互不干扰。前端打包用npm run build产物在dist目录。部署方式两种一是丢到 Nginx 的静态目录配置try_files $uri $uri/ /index.html解决刷新 404Vue 路由用 history 模式必踩二是放到 Spring Boot 的static目录里打成一个包省事但前后端耦合。Nginx 反向代理的配置大致是server { listen 80; server_name _; location / { root /var/www/activity/dist; index index.html; 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; } }client_max_body_size这行很容易漏默认值 1M用户上传一张稍微大点的活动封面图就报 413而且是前端看不到具体原因的那种报错。数据库方面MySQL 8 建库时显式指定CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci然后导入建表脚本和初始化数据。建议把建表和初始化数据分成两个 sql 文件一个schema.sql一个data.sql答辩前方便重置演示数据。4.2 演示数据怎么准备才像真的演示数据是最容易被忽视、但影响观感最大的一环。如果点进去活动列表只有三条数据标题是“测试活动1”“测试活动2”页面再漂亮也显得空。我的做法是准备 25 到 30 条活动数据覆盖不同分类讲座、文体、志愿、竞赛、社团纳新不同状态报名中、已结束、待审核、已取消各占一部分时间分布在过去两个月到未来一个月之间。标题写成真实活动会用的那种比如“春季羽毛球友谊赛报名”“图书馆志愿整理活动第 3 期”。组织者昵称、分类图标也一起配上。报名和签到数据也要造。报名中状态的活动造五六个人报名已结束的活动造十几条签到记录这样统计页面才有曲线可看。数据可以用 SQL 脚本批量插入也可以用自己写的接口一条条点但一定要在演示前跑一遍确认数据之间的关系是对的比如报名人数和活动表的signed_count对得上。提示准备两套数据一套完整版用于正常演示一套极简版用于现场卡顿时快速重置。重置脚本一条TRUNCATE加重新导入三十秒能恢复。5. 常见问题与排查实录5.1 时间差 8 小时、格式错乱这些坑时间问题几乎每个项目都会遇到而且症状很迷惑数据库里存的是对的页面上显示差 8 小时或者反过来。根因通常是三个地方没对齐数据库连接串的时区、JVM 的时区、序列化框架的时区。MySQL 连接串加上serverTimezoneAsia/ShanghaiJVM 启动参数加-Duser.timezoneAsia/ShanghaiJackson 配置里指定spring.jackson.time-zoneGMT8和日期格式yyyy-MM-dd HH:mm:ss。三处对齐基本就不会出问题。还有一个更隐蔽的实体类里用java.util.Date而数据库字段是datetime精度到秒用LocalDateTime配datetime(3)就到毫秒。两者混用会出现“存进去是 10:00:00.123查出来是 10:00:00”。统一用LocalDateTime加datetime简单省事。前端展示时间的另一个小技巧不要直接把后端返回的字符串塞到页面上用dayjs或者浏览器原生Intl.DateTimeFormat格式化一下顺便处理一下“今天 / 昨天 / 3 天后”这种相对时间显示观感提升明显。5.2 文件上传与静态资源访问活动封面图是这套系统的门面做好了这个页面立刻显得完整。但上传这块坑也不少。第一个坑是路径问题。本地开发时文件存到D:/upload部署到服务器上这个路径根本不存在。解决方案是配置一个可读写的绝对路径通过配置文件注入不要硬编码。我一般配成file.upload-path/data/upload/代码里读配置。第二个坑是访问不到。文件存进去了但页面上的图片是裂的。原因是没有配置静态资源映射。Spring Boot 里加一个配置类把/upload/**映射到实际目录Configuration public class WebMvcConfig implements WebMvcConfigurer { Value(${file.upload-path}) private String uploadPath; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath); } }注意file:前缀不能少少了会当成 classpath 路径去找永远 404。第三个坑是文件名冲突和安全性。用户上传的文件名千奇百怪带中文、带空格、带特殊符号直接用原名存很容易出问题。统一改成 UUID 加原始后缀同时在入库时存一份原始文件名用于展示。后缀也要做白名单校验只允许 jpg、png、webp、gif 这几种别信前端传来的 MIME 类型。5.3 答辩会被追问的几个点及回答思路这部分我按“老师最可能问什么、怎么答才不虚”整理成了一张表。答不上来的核心原因通常是“功能是抄的、没想过为什么”。追问点常见错误回答建议回答方向名额并发怎么处理的用了事务说明条件更新加唯一索引双保险讲清为什么先查后改会超卖为什么不用 Redis 缓存没学过说明当前数据量和并发量下数据库足够缓存引入一致性成本讲清取舍权限怎么设计的加了 if 判断说明 URL 拦截加数据级校验两层讲清为什么数据级校验必须放 Service活动取消后报名怎么办没考虑说明报名记录保留并置为已取消同时释放名额、发通知数据库为什么这么设计按功能建的从实体、关系、日志三类讲说明冗余字段是为了列表性能有什么创新点功能比较全讲具体的小点场地冲突检测、签到时间窗口、报名批量审核回答技巧上主动说出方案的缺点往往比硬吹更有说服力。比如被问“为什么不做分布式”可以说“做了单机压测100 并发下响应时间在 200ms 以内当前规模用不上分布式引入后运维和一致性成本反而更高”。这种回答体现的是判断力老师一般会给正面反馈。5.4 常见问题速查表把开发过程中反复出现的问题整理成表遇到时直接对照排查能省不少时间。现象可能原因排查与解决接口 401但已经登录token 未带或已过期拦截器路径配置错看请求头有没有 Authorization检查放行路径白名单跨域报错前端代理未配置或后端 CORS 未开开发环境用 vite/webpack 代理生产用 Nginx 同域列表分页总数不对count 查询漏了条件或 join 出错单独写 count 语句条件与列表查询保持一致图片 404静态映射路径或file:前缀缺失检查映射配置浏览器直接访问图片 URL 测试中文乱码字符集不是 utf8mb4 或连接串未指定建库建表、连接串、响应头三处统一报名人数和列表对不上signed_count未同步维护检查取消报名逻辑是否减一写个对账 SQL 比对打包后页面空白静态资源路径配置publicPath不对检查base配置用相对路径/定时任务重复执行多实例部署或任务未加锁单机部署确认只有一个实例加分布式锁或改用数据库标记对账 SQL 是我常用的一个小工具专门用来验证冗余字段有没有跑偏SELECT a.id, a.title, a.signed_count, (SELECT COUNT(*) FROM signup s WHERE s.activity_id a.id AND s.status APPROVED) AS real_count FROM activity a HAVING a.signed_count real_count;正常情况下这条查询应该返回空。如果返回了数据说明某处逻辑漏了同步顺着 id 去查操作日志就能定位。6. 我在做这类项目时的一点体会做完几套之后我最大的感受是这类系统的难度不在技术在“把一件事讲圆”。代码能跑只是及格线能不能说清楚每个设计背后的取舍才是拉开差距的地方。源码部分我建议把项目结构整理得干净一些controller、service、mapper、entity、dto、config 分层清楚公共返回体、全局异常处理、参数校验这些基础设施提前搭好。这些东西看起来是“非功能代码”但它们决定了后面写业务时顺不顺手。尤其全局异常处理有了它业务代码里只管抛BizException返回格式统一前端处理起来也简单。另外一个小建议从第一天就开始写文档不是最后补。每做完一个模块花十分钟记录一下接口、表结构变更、遇到的问题最后写论文的时候这些笔记就是素材库。我见过太多人代码写完了回头想“当时为什么要加这个字段”完全想不起来只能瞎编。最后分享一个让演示更稳的小技巧答辩前把整个流程从“注册新用户”开始完整走一遍包括异常路径——报名已满的活动、重复报名、审核驳回后重新提交、取消活动。这些路径走得通说明系统是真的完整走不通就趁早修别等到现场手忙脚乱。