资讯动态

从增删改查到并发权限:学生活动管理平台毕业设计落地

发布时间:2026/10/27 7:31:53 来源:尧图企业网站定制
学生活动管理平台这个题目几乎每年毕业设计季都会被大量选中但真正能把它做出“能跑、能讲、能答辩”三个层次的人并不多。大部分人的成品停在“活动增删改查 报名列表 一个后台管理页”代码能启动但一被问到“名额抢完了怎么办”“审批被驳回之后活动是什么状态”“学生干部能看到别院系的数据吗”就答不上来了。我前后帮人看过不少这类项目也在自己的实践里完整走过一遍从开题报告到源码交付的流程踩过的坑基本都集中在几个固定位置需求边界没划清、表结构设计得太随意、并发场景没考虑、权限只做了一个 role 字段。这篇内容面向的是正在做或者准备做这个题目的同学也适合任何想做一个“报名类、审批类”信息系统的开发者参考。我会把重点放在三件事上一是这个题目里真正有技术含量的模块到底是哪几个二是数据库和并发这类硬骨头怎么啃三是开题报告和源码之间怎么对得上让评审和答辩环节少掉分。中间会穿插可直接复用的建表语句、核心方法和技术选型理由尽量做到拿来就能改。1. 这个题目的难点不在增删改查而在需求边界1.1 需求文档里最常缺的那几块我见过的大部分学生活动管理平台的需求描述基本是这么写的管理员发布活动学生浏览活动并报名管理员审核报名活动结束后统计参与人数。四句话结束。问题在于这四句话里藏着至少十几个没定义清楚的分支而这些分支恰恰是系统设计的核心。举几个具体的活动发布之后能不能改时间改了时间之后已经报名的学生要不要重新确认报名需不需要审核还是先到先得名额满了之后是直接关闭还是进候补队列活动开始前多久停止报名学生报名后不来要不要记录违约一个学生能不能同时报同一个活动的多个场次活动是院系级的还是全校级的跨院系能不能报名这些问题的答案直接决定了表结构、接口数量和前端页面数量。我的建议是在正式写代码前先用一张表格把所有状态和流转列出来哪怕只是手写。因为这决定了你后面数据库里要放几个状态字段而不是事后发现状态不够用再去加字段、改判断逻辑那时候改起来成本翻倍。1.2 把三类角色和四条主流程钉死角色这件事一定要收敛。很多项目一上来就设计“超级管理员、管理员、院系管理员、辅导员、学生干部、普通学生”六种角色结果做到一半发现权限矩阵写不完每个接口都要判断一堆 if。我的做法是收敛成三类主体平台管理员负责账号、院系、活动类型这类基础数据、活动组织者发布活动、管理报名、组织签到通常由学生干部或指导教师担任、学生浏览、报名、取消、签到、评价。如果确实需要院系隔离不要新增角色而是在账号上加一个“所属院系”属性用数据权限来控这个后面第 4 章会展开讲。四条主流程则是活动从草稿到发布到结束的生命周期、报名从提交到通过或拒绝到签到、审批从发起到流转到归档、统计从原始记录到汇总报表。把这几条线画清楚页面数量基本就定了不会出现做到一半发现漏了一个模块的情况。1.3 技术选型为什么最后都落在 Spring Boot 加 Vue 这套组合选型这块我不打算罗列一堆框架做对比表因为对这个题目来说选型的核心约束只有三个上手成本、资料密度、答辩时可解释性。后端用 Spring Boot 的理由很实际它把配置、依赖注入、Web 层、数据访问层全部收敛到一套约定里你不需要自己搭 Servlet 容器一个 main 方法就能起服务。对一个需要在两三周内出成品的项目来说这种“少折腾”比性能重要得多。数据访问层用 MyBatis-Plus 或者 MyBatis前者自带通用 CRUD 和分页插件能省掉大量模板代码但如果你的开题报告里写了“研究 MyBatis 映射机制”之类的点那就老老实实用原生 MyBatis 写 XML别让报告和代码对不上。前端选 Vue 的核心原因是组件化和生态成熟。活动列表、报名表单、后台表格这些页面结构高度重复用组件抽出来之后改一处就能全局生效。搭配 Element Plus 这类组件库表格、分页、弹窗、表单校验基本开箱可用。如果团队里有人更熟 React那用 React 也完全没问题但不要在开题报告里写 Vue、代码里用 React这种低级不一致在评审环节非常扎眼。数据库就用 MySQL原因是这个系统本质上是一堆关系明确的结构化数据活动、用户、报名、审批天然适合关系型建模用文档数据库反而要把关联查询写到应用层得不偿失。2. 数据库设计把“活动”这个名词拆到不能再拆2.1 核心表的拆分逻辑很多人建表的时候习惯一张activity表塞下所有东西活动名、时间、地点、人数、组织者、审核状态、报名列表。这是后面所有问题的根源。我的拆分思路是按“名词的生命周期”来切。活动本身是主体报名是活动和用户之间的关系审批是活动的一个附属流程签到是报名的延伸。对应下来大致是这么几张表表名职责关键点sys_user账号主体角色、所属院系、状态activity活动主表状态机字段、名额、时间窗activity_signup报名记录用户与活动唯一约束、签到状态activity_approval审批流水一次审批一条可追溯activity_category活动分类便于统计和筛选sys_notice通知消息与业务事件解耦这里有个细节值得强调审批不要做成活动表上的一个 status 字段。因为审批可能是多级的也可能被驳回后重新提交一个字段存不下历史。做成流水表每次操作插一条记录不仅能还原整个审批链路答辩的时候演示起来也更有说服力。同理签到状态不要放在活动表上它属于报名记录一个活动有几十上百条签到状态放主表根本存不下。2.2 那些必须提前加的字段这一节是我认为最值得抄的部分。以下几个字段如果一开始不加后面补起来会很痛苦create_time/update_time每张业务表都要有。不是为了展示是为了排查问题时能定位到“这条数据是什么时候被改的”。用 MySQL 的DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP可以自动维护。deleted逻辑删除活动被“删除”时如果物理删除报名记录就变成孤儿数据统计报表直接错乱。加一个 0/1 标记查询时过滤掉即可。version乐观锁版本号这个字段平时不用但在报名扣名额的时候是救命稻草后面会详细讲。remark几乎每个业务表都会在某个时刻需要一句备注事后加字段要改实体、改 mapper、改前端不如一次加上。还有一类容易漏的是时间窗字段。活动表里不要只存一个start_time至少要有signup_start_time、signup_end_time、activity_start_time、activity_end_time四个。报名是否开放、活动是否进行中、能否签到全部由这四个时间和服务端当前时间比较得出而不是靠一个“是否开放报名”的开关字段手动维护。因为开关字段一定会忘记改。活动主表的建表语句大概长这样可以直接拿去改CREATE TABLE activity ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, title VARCHAR(120) NOT NULL COMMENT 活动名称, category_id BIGINT NOT NULL COMMENT 分类, cover_url VARCHAR(255) DEFAULT NULL COMMENT 封面图相对路径, description TEXT COMMENT 活动详情, location VARCHAR(150) NOT NULL COMMENT 活动地点, organizer_id BIGINT NOT NULL COMMENT 组织者用户ID, dept_id BIGINT DEFAULT NULL COMMENT 归属院系用于数据隔离, quota INT NOT NULL DEFAULT 0 COMMENT 名额上限, signed_count INT NOT NULL DEFAULT 0 COMMENT 已报名人数冗余, signup_start DATETIME NOT NULL COMMENT 报名开始时间, signup_end DATETIME NOT NULL COMMENT 报名截止时间, start_time DATETIME NOT NULL COMMENT 活动开始时间, end_time DATETIME NOT NULL COMMENT 活动结束时间, status TINYINT NOT NULL DEFAULT 0 COMMENT 0草稿 1待审 2已发布 3已驳回 4已结束 5已取消, need_audit TINYINT NOT NULL DEFAULT 0 COMMENT 报名是否需要审核, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本, deleted TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_status_signup (status, signup_end), KEY idx_dept (dept_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT活动主表;signed_count这个冗余字段是有意加的。如果每次都去activity_signup里 count 一遍活动列表页一刷新就是几十次子查询页面响应会很难看。冗余字段的代价是必须保证它和明细一致这一点靠后面要讲的并发处理来保证。2.3 索引和唯一约束让数据库替你兜住一部分 bug我特别想强调唯一约束这件事。报名表上有一组唯一索引是必须要有的UNIQUE KEY uk_activity_user (activity_id, user_id, deleted)这条索引能直接挡掉一类最常见的脏数据同一个学生对同一个活动重复报名。你把逻辑写在 Java 里做“先查再插”在高并发下是一定会漏的两个请求同时查到“没报过”然后都插进去。加上唯一索引之后第二个插入会抛异常你在代码里捕获这个异常给用户返回“你已报名”逻辑就闭环了。需要注意deleted要放进唯一索引里否则用户取消报名逻辑删除之后再想重新报名会撞上唯一约束。常见的做法就是用物理删除、或者把 deleted 设计成一个时间戳/自增序号参与唯一键具体用哪种看你的业务对历史记录的依赖程度。我个人倾向于报名记录保留物理行用一个signup_status字段标记“已取消”唯一索引就建在(activity_id, user_id)上取消后再报名就是 update 状态而不是 insert逻辑更干净。索引方面活动列表页最常见的查询是“某状态 报名未截止”所以(status, signup_end)这个联合索引很有价值。院系隔离的系统还要在dept_id上建索引。别一股脑给所有字段加索引写操作会变慢而且维护成本高。3. 报名名额的并发问题从超卖到幂等3.1 先把问题复现出来假设名额 50 个不需要审核先到先得。你的代码大概率是这么写的Activity activity activityMapper.selectById(id); if (activity.getSignedCount() activity.getQuota()) { throw new BizException(名额已满); } // 插入报名记录 // 更新 signed_count 1单线程跑一万遍都没问题。但只要人数一多就会出现“50 个名额报进去 53 个人”的情况。原因很直接两个请求同时读到signed_count 49都判断通过都去加 1最终结果可能是 51而实际插入了 2 条记录。这个现象有个专业名词叫超卖在秒杀系统里是经典问题但在学生活动场景里同样存在——尤其是那种热门讲座、演唱会门票式的活动开放报名的那一分钟并发量其实很可观。复现方法很简单用压测工具开 100 个线程同时打报名接口把名额设成 10然后看数据库里到底插了多少条。我建议每个做这个题目的人都亲手做一次这个实验因为只有亲眼看到数据错乱才会真正理解为什么要做并发控制。3.2 三种方案的取舍解决方案我知道的有三个从简单到复杂分别是方案一数据库行锁悲观锁。在事务里先SELECT ... FOR UPDATE把活动这行锁住再判断再扣减。SELECT * FROM activity WHERE id #{id} FOR UPDATE;优点是实现最简单逻辑绝对正确。缺点是锁的粒度大同一个活动的所有报名请求串行执行吞吐量上不去。但对于校级活动这种量级几百人同时抢完全够用我非常推荐直接用它别过度设计。方案二乐观锁。用上面那个version字段更新时带上版本号UPDATE activity SET signed_count signed_count 1, version version 1 WHERE id #{id} AND version #{version} AND signed_count quota;看影响行数如果是 0 就说明被别人抢先了返回失败或者重试。这个方案并发性能更好但需要处理重试逻辑而且要注意signed_count quota这个条件必须在 SQL 里不能只在 Java 里判断。方案三缓存预扣。用缓存中间件常见的是 Redis做名额计数器先原子递减减成功了再落库。这是高并发系统的标准做法但对这个题目来说属于明显过度设计除非你的开题报告里明确写了“研究高并发场景下的名额控制”否则不建议上。引入缓存之后的一致性问题扣了缓存但落库失败怎么办、缓存重启怎么办会让你多花一倍时间。我个人在这个题目上的建议是用乐观锁做好失败提示。理由是它比悲观锁性能好比缓存方案简单而且在答辩时能讲清楚原理是个加分项。下面给出核心代码。3.3 唯一索引加状态机真正的兜底乐观锁解决的是signed_count的准确性问题但报名记录本身的重复插入还要靠唯一索引。完整的报名流程我一般这么写Transactional(rollbackFor Exception.class) public void signup(Long activityId, Long userId) { Activity act activityMapper.selectById(activityId); // 1. 基础校验状态、时间窗、是否本人 if (act null || act.getStatus() ! 2) { throw new BizException(活动不存在或未开放报名); } LocalDateTime now LocalDateTime.now(); if (now.isBefore(act.getSignupStart()) || now.isAfter(act.getSignupEnd())) { throw new BizException(不在报名时间内); } // 2. 已报名直接返回保证幂等 Long cnt signupMapper.countByActivityAndUser(activityId, userId); if (cnt 0) { throw new BizException(你已报名该活动); } // 3. 乐观锁扣名额影响行数为 0 说明抢完了 int rows activityMapper.increaseSignedCount(activityId, act.getVersion()); if (rows 0) { throw new BizException(名额已满请稍后再试); } // 4. 插入报名记录唯一索引兜底 ActivitySignup record new ActivitySignup(); record.setActivityId(activityId); record.setUserId(userId); record.setSignupStatus(act.getNeedAudit() 1 ? 0 : 1); // 0待审 1已通过 record.setCheckinStatus(0); try { signupMapper.insert(record); } catch (DuplicateKeyException e) { throw new BizException(你已报名该活动); } }注意这里的顺序先扣名额再插记录。如果反过来的话记录插进去了但名额没抢到就得回滚事务里多做一次无效写。不过先扣后插也有风险——插记录失败要回滚名额所以整个方法必须在同一个事务里rollbackFor一定要写。还有一点第 2 步的“先查”只是为了给用户一个友好的提示真正防止重复的是第 4 步的唯一索引。这两者不冲突前者提升体验后者保证正确。3.4 取消报名和名额回补取消报名看起来简单其实有两个坑。第一个坑是时间窗。活动开始前 24 小时还能不能取消这个规则必须写进代码否则会出现活动已经开始了还有人取消名额回补了却没人能用。我的做法是在取消接口里判断now activity.getStartTime()就拒绝必要时可以再收窄到“开始前 N 小时”。第二个坑是重复回补。如果列表页上取消按钮能被连点两次signed_count就会被减两次。解决方式还是状态机取消时用带条件的 UPDATE。UPDATE activity_signup SET signup_status 2, cancel_time NOW() WHERE id #{id} AND signup_status IN (0, 1);影响行数为 1 才继续去减signed_count为 0 说明已经取消过了直接返回。这样即使并发点两次也只有一次生效。名额回补的更新语句也要加保护防止减成负数UPDATE activity SET signed_count signed_count - 1 WHERE id #{id} AND signed_count 0;4. 审批流与权限模型一个 role 字段扛不住的东西4.1 RBAC 的最小实现权限这块很多人第一反应是在用户表上加一个role字段值是admin或者student。少量接口这么干可以但只要出现“组织者只能管自己发布的活动”“院系管理员只能看本院系数据”这类需求这个字段就不够用了。最小可用的做法是三张表用户、角色、用户角色关联。菜单和按钮权限再加一张权限表和角色权限关联表。但对于这个题目我建议只做到“角色 数据范围”两层不要上完整的菜单级权限那是后台管理框架的活儿跟你这个系统的核心价值没关系。CREATE TABLE sys_role ( id BIGINT NOT NULL AUTO_INCREMENT, role_code VARCHAR(32) NOT NULL COMMENT ADMIN / ORGANIZER / STUDENT, role_name VARCHAR(64) NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_role_code (role_code) );权限判断放在哪一层是个设计问题。放在 Controller 里最直观但会散落到各个方法放在拦截器或者切面里更统一但需要定义好注解。我用的是自定义注解加拦截器的方案在方法上标RequireRole(ORGANIZER)拦截器解析注解比对当前登录用户的角色集合。这样既直观又不重复。4.2 活动审批的状态流转审批这件事如果不把状态画清楚代码里会写出一堆互相矛盾的 if。我的做法是先定义状态枚举然后强制所有状态变更都走一个方法不允许别处直接 set。活动侧的状态是草稿 → 待审 → 已发布 / 已驳回发布之后还能到已结束或已取消。驳回之后可以改完重新提交回到待审。这里有个细节已驳回的活动能不能直接编辑能但要保留驳回原因给组织者看所以审批流水表里要存comment字段。审批流水的表结构CREATE TABLE activity_approval ( id BIGINT NOT NULL AUTO_INCREMENT, activity_id BIGINT NOT NULL, approver_id BIGINT NOT NULL, action TINYINT NOT NULL COMMENT 1通过 2驳回, comment VARCHAR(500) DEFAULT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_activity (activity_id) );驳回原因一定要展示给组织者否则他改都不知道改哪儿这个体验细节在演示环节很容易被注意到。4.3 数据权限看得见和改得动是两件事这是我在这个项目里踩得比较深的一个坑。一开始我把“能不能看”和“能不能改”混在一起判断结果出现了组织者能看到别院系活动、但点进详情又报错的情况。正确的拆分是两层可见性决定列表和详情能不能返回可操作性决定编辑、删除、审核这些写操作能不能执行。可见性用查询条件拼 SQL 实现比如非管理员一律加上dept_id #{currentDeptId}或者organizer_id #{currentUserId}。可操作性用注解拦截。还有一个容易忽略的点详情接口和列表接口的权限必须一致。很多系统列表页做了过滤但详情接口直接按 ID 查把 URL 里的 ID 一改就能看到别人的数据。这种越权是最常见的漏洞之一答辩时如果被问到“你怎么防止越权访问”能答出这一条会很有说服力。具体做法是在详情接口里拿到数据后再判断一次当前用户是否在可见范围内不在就返回 403。这一行判断代码只有几行但能挡住绝大多数问题。5. 签到、统计与通知让系统看起来是活的5.1 签到码的设计取舍签到功能是整个系统里最能出效果的部分也是设计上最容易走偏的部分。最粗糙的做法是活动详情页放一个固定的六位数字码学生输入正确就算签到。问题是这个码会被截图转发一个人签到完发给全寝室统计就废了。稍微好一点的做法是动态码页面定时刷新一个带时效的码服务端校验时间窗口。再进一步是二维码加签名 tokentoken 里编码活动 ID、时间戳、随机串服务端验签并检查时间窗口。我的建议是用二维码而不是数字码因为二维码的视觉效果好演示时很加分而且实现难度并不高。生成二维码只需要一个工具库把一段字符串编码成图片返回给前端即可。服务端校验逻辑大概是这样token 里带activityId expireAt sign签名的密钥放在服务端配置里。学生扫码后前端把 token 传给后端后端验签、比对活动 ID、检查是否过期然后更新报名记录的签到状态。public void checkin(String token, Long userId) { CheckinToken t tokenParser.parse(token); // 验签 解析 if (t.getExpireAt() System.currentTimeMillis()) { throw new BizException(签到码已失效请让组织者刷新); } int rows signupMapper.updateCheckin(t.getActivityId(), userId); if (rows 0) { throw new BizException(未找到你的报名记录); } }updateCheckin里同样要带条件AND checkin_status 0保证重复扫码不会重复计数。5.2 统计查询怎么写才不慢后台首页一般要有几个数字活动总数、本月活动数、报名总人次、平均签到率。这些如果用一条条count(*)去查页面会明显卡顿。我的做法是活动总数、报名总数这类全局指标直接查因为数据量不大签到率这类需要关联统计的用一条 GROUP BY 语句一次性算出来SELECT a.id, a.title, a.quota, COUNT(s.id) AS signup_cnt, SUM(CASE WHEN s.checkin_status 1 THEN 1 ELSE 0 END) AS checkin_cnt FROM activity a LEFT JOIN activity_signup s ON s.activity_id a.id AND s.signup_status 1 WHERE a.deleted 0 AND a.status 4 GROUP BY a.id, a.title, a.quota ORDER BY a.end_time DESC LIMIT 20;注意activity_signup上要有(activity_id, signup_status)的联合索引否则每次统计都是全表扫描。还有一个坑是统计口径。报名人次和报名人数不是一回事一个人报了三场活动人次算 3人数算 1。这两个数字在报表里经常被混用导致对不上。我建议在页面上把标签写清楚比如“报名人次”避免歧义。5.3 通知消息怎么落地通知功能很多人做成一个独立的消息模块写一堆站内信、已读未读。对这个题目来说够用就好。我的方案是事件驱动的最小实现活动审核通过、活动被驳回、报名成功、活动即将开始这四个节点触发通知。通知表结构就是user_id title content read_flag create_time前端在右上角显示未读数点开是一个列表。关键在于“触发”这一步放在哪。如果散落在各个 Service 里直接调noticeService.send()后面加一种通知就要改好几处。更好的做法是发布应用内事件用监听器统一处理。这样业务代码里只需要publishEvent(new ActivityApprovedEvent(activityId))通知内容怎么组装、发给谁都在监听器里做。要不要做邮件或短信我建议不做。一来要配置第三方服务二来演示时容易出问题三来对答辩没有任何加分。站内通知足够了。6. 开题报告和源码怎么对得上6.1 开题报告里哪些内容可以后补哪些必须前置开题报告的固定结构一般是研究背景与意义、国内外现状、研究内容与目标、技术路线与方案、进度安排、参考文献。我按“先写后写”的优先级给个排序。必须前置写清楚的是研究内容和功能模块列表因为这是你后面写代码的地图。如果报告里列了六个模块代码里只有四个答辩时一定会被追问。反过来如果代码里做了一个报告里没提的特色模块那是个加分项说明你有额外探索。可以后补的是技术路线里的具体版本号、数据库表清单、接口文档。这些在开题阶段写到“使用 Spring Boot 搭建后端服务MySQL 存储业务数据”这个粒度就够了细节留到实现阶段再补。最容易写空的是国内外现状。这一节如果写成“国外起步较早技术成熟国内发展迅速应用广泛”基本等于没写。我建议换个角度不要泛泛谈“学生活动管理系统”而是聚焦到你实现的某个技术点。比如谈“活动报名的并发控制方案”或者“基于角色的细粒度权限模型”这个话题在公开资料里有足够多的方案可以比较写起来就有内容而且直接支撑你后面的设计决策。6.2 源码结构怎么组织才不显得像拼的评审和答辩老师看代码第一眼看的就是目录结构。如果controller下面混着工具类utils里塞着业务逻辑那给人的印象就是“东拼西凑”。我用的分层是标准的四层controller只做参数校验和响应封装、service业务逻辑、mapper数据访问、entity数据对象再加上config、common、exception、dto这几个辅助包。DTO 和 Entity 一定要分开请求参数用XxxQuery、XxxForm返回用XxxVO。有人觉得这是过度设计但在答辩场景里这套分层本身就是“我懂规范”的信号。另一个建议是把 README 认真写。至少要包含项目简介、技术栈版本、数据库初始化脚本位置、启动步骤、默认账号密码、功能模块截图。很多项目的 README 只有一行“学生活动管理系统”看一眼就不想往下翻了。6.3 答辩现场最可能被问到的几个问题我把这些年听过的高频问题整理一下每个都给出回答方向“你的项目和网上现成的有什么区别”别说“我加了签到功能”太弱。要答具体的技术决策比如“我在报名模块用乐观锁加唯一索引解决了并发超卖和重复报名这是很多同类项目没处理的”。“数据库为什么这么设计符合三范式吗”老老实实说哪几张表做了冗余、为什么冗余比如signed_count是为了避免列表页子查询这比硬说“完全符合三范式”可信得多。“如果同时有一千个人报名怎么办”分层次答先用唯一索引挡重复再用乐观锁控名额量再大可以引入缓存预扣。能说出这个递进关系就很好。“权限是怎么做的”答 RBAC 加数据范围重点提“列表过滤 详情二次校验”防止越权。“你做了测试吗”不要空口说做了直接说“我用压测工具模拟了 100 并发报名 10 个名额的场景修复前出现超卖修复后计数准确”这个回答的含金量非常高。7. 实际跑起来之后才会遇到的几个坑7.1 时间字段的时区和格式这个问题几乎人人都踩。表现是数据库里存的时间是对的但页面上显示差了 8 小时或者反过来。根因在于 MySQL 连接串没指定时区、Java 里用了java.util.Date、序列化时又没配格式三者任意一个不对都会出问题。我的处理是全部统一数据库连接串加上时区参数实体类里用LocalDateTime全局配置序列化格式为yyyy-MM-dd HH:mm:ss。这样从存储到传输到展示是一条线不会出现某处差 8 小时。spring: datasource: url: jdbc:mysql://127.0.0.1:3306/activity_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai还有个小细节时间比较一定要在服务端做不要信任前端传上来的“当前时间”。前端时钟可以随便改服务端用自己系统时间判断才可靠。7.2 图片上传和路径回显上传功能看起来简单坑却不少。第一个坑是存本地还是存对象存储。这个题目我建议存本地放在一个不随打包变化的目录里数据库只存相对路径比如/upload/2024/05/xxx.jpg。配置一个静态资源映射把 URL 前缀映射到磁盘目录前端直接用这个相对路径访问。第二个坑是前端路由和后端静态资源打架。如果你的前端是 history 模式刷新页面会出现 404如果后端拦截器配置了统一前缀静态资源也会被拦。解决方式是配置静态资源放行同时前端路由加上 fallback 到 index.html。第三个坑是文件名冲突。用户上传的原始文件名可能重复必须重命名用时间戳加随机串同时保留扩展名做类型校验。只靠前端限制文件类型是不够的后端也要检查扩展名否则传个脚本上去会有安全问题。7.3 列表分页和导出分页这块如果用 MyBatis-Plus 的分页插件记得配置拦截器否则Page对象不会生效会查全表返回。这个问题我第一次用的时候就踩了页面能显示但总数永远是全部数据。导出的话数据量小几千行以内直接查出来写 Excel 就行用现成的工具库一行代码搞定。数据量大就要分批查每批 5000 条写一次避免内存溢出。另外导出的字段名要做中文映射别把signed_count这种字段名直接暴露给用户看。还有一个细节导出接口如果直接GET带着一堆查询条件参数一长 URL 会超限。要么改成 POST要么把条件做短。7.4 部署和首次启动最后说一下跑起来这件事。开发环境和部署环境最大的区别是数据库初始化和文件路径。数据库脚本一定要单独放一个sql目录包含建表语句和基础数据角色、分类、默认管理员账号。别人拿到你的项目第一步就是执行这个脚本如果脚本里有语法错误或者缺字段整个项目就跑不起来。前端打包之后要配置请求地址。如果后端和前端同域部署直接配反向代理把/api转发到后端端口就行如果是分开部署要在打包配置里改环境变量。最常见的问题是打包之后接口全 404十有八九是反向代理的前缀没配对。还有一点application.yml里的数据库密码不要写死成自己本地的要么用环境变量覆盖要么在 README 里明确写清楚要改哪里。这个细节虽然小但会让拿到项目的人少折腾半小时。我自己做这类系统的体会是功能列表永远可以再加但稳住核心的三条线——报名的数据准确性、审批的状态清晰度、权限的边界——才是这个项目真正的技术含量所在。很多看起来“功能很多”的成品一旦被问到某条数据是怎么来的、为什么是这个状态就露怯了。反过来功能不多但每条链路都闭环、每个状态都能说清楚为什么这么设计答辩的时候底气完全是两回事。报名模块的压测实验我建议一定要亲手做一遍数据从错乱到准确的这个过程比看十篇讲并发的文章都有用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑