
简介一份面向高校计算机、电子信息工程等专业学习者的研究生管理系统Java源码包基于SSMSpringSpringMVCMybatis框架与Vue前端构建采用B/S架构和MVC分层设计适配JDK1.8、MySQL5.7、Maven3.6及Tomcat8/9环境适合作为毕业设计、课程设计或期末大作业。压缩包共805个文件大小约21.11MB其中216个Java文件承载后端业务逻辑154个Vue组件实现前端交互33个XML配置负责SSM框架整合另有SQL数据库脚本、JS/CSS静态资源、SVG图标及bat构建启动脚本目录结构清晰便于按模块理解和二次开发。当前已有54人学习下载源码经过严格测试可直接导入IDEA/Eclipse等开发工具运行。借助这份源码读者能获得完整的研究生管理业务前后端实现、数据库初始化脚本与工程级配置既适合在毕业答辩中演示核心功能也能作为SSMVue实战项目进行功能扩展与重构是快速上手企业级Java Web开发的实用参考。1. 研究生管理系统代码从一条学籍记录到一篇学位论文研究生管理系统代码在网上一搜就是一大把但大多数打开之后会发现权限写死、流程靠前端按钮硬撑、换了个学院就翻车。研究生管理这件事本质上不是一套“学生信息增删改查”而是一条从注册、选导师、开题、中期、答辩到学位申请的完整链路每一环都牵扯学生、导师、教务三个角色。用 Java 技术栈落地这套系统Spring Boot MyBatis Plus 是当前最主流、也最适合拿来当毕设或课设的组合。这篇文章写给两类人正在做 java 研究生毕设项目、需要交付一套能演示系统的在校生以及想用真实业务把自己从 CRUD 里拽出来的初级 Java 工程师。读完你会有能建表的 SQL、能抄的鉴权与状态机代码也知道哪些坑值得提前绕着走。2. 技术选型与数据库设计Spring Boot 三件套和六张核心表研究生管理系统是一个典型的权限密集、流程密集、数据关系密集的业务系统。选型如果只在“能不能跑”上打转后面改需求时会非常痛苦。这章先把组合怎么选、表怎么拆、表结构怎么落地讲清楚这些是后面所有模块的地基。2.1 为什么是 Spring Boot MyBatis Plus 轻量前端先回答最现实的问题为什么这套组合成了研究生管理系统的“默认答案”。Spring Boot 负责把配置收敛起来内嵌 Tomcat、起步依赖、Actuator 健康检查这些都让部署成本大幅降低一个mvn spring-boot:run就能把后端拉起来。MyBatis Plus 的价值更直接单表 CRUD 不需要手写 SQL内置分页插件、代码生成器、逻辑删除、乐观锁能把一个管理类项目里七成以上的样板代码省掉。剩下的精力全部投入到权限模型和流程状态这两块真正容易写崩的地方。前端的选择我一般会给两条路。如果你是一个人做横评类项目时间紧任务重直接用 Thymeleaf 模板加 Bootstrap后端渲染部署时只有一个 Jar不用处理跨域不用起两个服务如果你想把系统当作求职作品那还是 Vue 3 Element Plus 做前后端分离更体面但代价是演示现场要多处理一层跨域和端口问题。我的建议是答辩项目优先保稳定前后端分离放到第二版再上。这个选择跟技术能力无关跟演示失败的概率有关。再说一句和学习路线的关系。如果你正照着 java 学习路线走到 Spring Boot 这一站商城类项目练的是 CRUD 和缓存而这个系统练的是权限、状态机、事务和并发控制——这些恰恰是工作里最容易出问题、面试里最常被追问的点。拿它当综合练习性价比很高。2.2 六张核心表与角色边界不要再把一切塞进 user 表很多半成品系统最大的问题是把所有信息堆在一张 user 表里role 字段、学院、导师工号、班级全塞进去最后查询靠like硬撑。研究生管理系统里人和角色是多对多的一个用户可以是学生也可以是管理员甚至可以是助教。常见做法是拆成“账号表 扩展表”用user_id关联。表名职责关键字段说明user账号与角色id, username, password, rolerole 取 admin/teacher/studentstudent学生扩展信息user_id, student_no, college, major, grade学号唯一索引teacher导师扩展信息user_id, teacher_no, title, research_area职称、研究方向用于双选展示teacher_quota导师名额teacher_id, total_quota, used_quota双选时的容量控制select_apply双选志愿student_id, teacher_id, round_no, priority, statuspriority 表示第几志愿paper_process论文流程student_id, type, status, title, submit_time, audit_time开题、中期、答辩共用一张表学生和导师的信息为什么拆出来单独建表因为user表只负责鉴权而 student 和 teacher 是业务实体它们的字段变化频率不一样学生要加一个“是否脱产”字段导师要加一个“可带学生数”如果都在 user 表里改一次就要动鉴权表风险很高。扩展表存在一定的冗余问题例如一个用户如果既在 student 表又在 teacher 表逻辑上会混乱所以实际落地时我会加唯一约束student.user_id和teacher.user_id都建唯一索引从数据库层面拦住“一个人既是学生又是导师”的脏数据。paper_process 这张表是整个系统的核心后面第 4 章展开讲。这里先记住它的设计原则不同类型流程共享同一张表用type区分而不是给开题、中期、答辩各建一张结构几乎一样的表这是后续状态机可以复用的前提。2.3 表结构落地从实体类到建表 SQL 的正确顺序很多人搜过“mybatisplus 根据 java 实体类生成创建表的 sql 语句”这种操作确实有工具能把实体类反向生成建表 SQL。但我一般不建议靠它直接上生产字段注释会丢、decimal 长度不准确、tinyint 的语义不明确生成出来的东西还要人工再审一遍。更稳的做法是反过来——先写schema.sql定义表结构再让代码生成器根据表生成实体表是唯一事实来源。下面这张paper_process的建表 SQL 可以直接抄注意几个容易被忽略的点version字段给乐观锁用idx_student_type联合索引支撑“查某个学生的全部流程”这类高频查询audit_comment允许为空但业务层会校验。字符集统一用 utf8mb4避免论文题目里出现生僻字或 Emoji 时乱码。CREATE TABLE paper_process ( id bigint(20) NOT NULL AUTO_INCREMENT, student_id bigint(20) NOT NULL COMMENT 学生ID关联 student 表, type varchar(20) NOT NULL COMMENT 流程类型thesis_topic/opening/midterm/defense, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0草稿 1待审核 2已通过 3已驳回, title varchar(200) DEFAULT NULL COMMENT 论文题目或报告标题, submit_time datetime DEFAULT NULL COMMENT 学生提交时间, audit_time datetime DEFAULT NULL COMMENT 审核时间, audit_comment varchar(500) DEFAULT NULL COMMENT 审核意见, version int(11) NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, PRIMARY KEY (id), KEY idx_student_type (student_id, type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT论文流程表;表结构定好后用 MyBatis Plus 的 FastAutoGenerator 根据表反向生成 entity、mapper、service、controller这一步能省大量重复劳动。生成器代码如下FastAutoGenerator.create(jdbc:mysql://localhost:3306/graduate, root, your_password) .globalConfig(builder - builder.author(demo).outputDir(src/main/java)) .packageConfig(builder - builder.parent(com.example.graduate)) .strategyConfig(builder - builder .addInclude(user, student, teacher, teacher_quota, select_apply, paper_process) .entityBuilder().enableLombok() .controllerBuilder().enableRestStyle()) .execute();这段代码的意思是按你指定的表批量生成三层代码entityBuilder().enableLombok()让实体类带上Data注解省去手写 getter/settercontrollerBuilder().enableRestStyle()生成 REST 风格的 Controller 骨架。要注意生成器产出的 Service 只是空壳extends ServiceImpl真正的双选校验、状态流转逻辑还是得手写。把生成器当成打字员别把它当成架构师。3. 登录鉴权与导师双选最容易写崩的两个模块登录鉴权和导师双选是所有研究生管理系统里出镜率最高、也是最容易翻车的两个模块。一个管“谁能用系统”一个管“学生和导师怎么建立关系”两者都牵涉多角色和多状态。这章给出可复用的写法并把参数边界说透。3.1 JWT 登录与三种身份的权限边界管理员、导师、学生三种角色权限差异非常明显学生只能看自己的信息和流程导师能看名下学生的流程管理员能看全院数据。权限模型我一般不用 Spring Security 的全套过滤器链而是用“拦截器 自定义注解 JWT”的组合原因很简单这套系统角色就三种用注解声明比写一堆 Security 配置更直观团队新人也容易接手。登录接口先做两件事用 Bcrypt 校验密码签发 JWT。密码不能是明文这是底线哪怕演示系统也一样。JWT 里只放 userId 和 role不放姓名、学院这些可变信息避免用户改名后 token 里的旧数据造成显示错乱。过期时间一般设 2 小时如果要连续演示一整个上午可以放宽到 12 小时。拦截器的核心逻辑如下Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 非控制器方法直接放行比如静态资源和错误转发 if (!(handler instanceof HandlerMethod)) { return true; } // 接口方法上没有 RequireRole 注解说明不需要登录 HandlerMethod handlerMethod (HandlerMethod) handler; RequireRole requireRole handlerMethod.getMethodAnnotation(RequireRole.class); if (requireRole null) { return true; } // 取 token 并校验 String token request.getHeader(Authorization); if (token null || !JwtUtil.verify(token)) { throw new BizException(401, 登录已过期请重新登录); } // 解析角色判断是否在允许列表里 Claims claims JwtUtil.parse(token); String role claims.get(role, String.class); if (!Arrays.asList(requireRole.value()).contains(role)) { throw new BizException(403, 当前角色无权访问); } // 请求期间保存用户信息Controller 和 Service 直接取 UserContext.set(claims.get(userId, Long.class), role); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 请求结束必须清理 ThreadLocal否则线程池复用会串数据 UserContext.clear(); } }代码逻辑不复杂但有两个参数细节值得注意。第一RequireRole注解加在方法上RequireRole({teacher, admin})表示只有导师和管理员能访问没加注解的接口默认放行——这意味着新增接口时必须刻意关注权限漏加就会裸奔如果团队担心漏加可以把拦截器改成默认拒绝、显式PermitAll才放行这是更严格的策略。第二UserContext.clear()必须放在afterCompletion不在finally里否则一次请求结束后没清理下一次请求用同一个线程就会拿到上一个人的 userId这种 bug 极其隐蔽现场排查起来非常痛苦。3.2 导师双选的数据模型志愿制、名额锁定与事务边界双选规则在不同学校差别很大最常见的是志愿制学生可以填若干个导师志愿导师在待确认列表里选择通过或拒绝也有学校是“先到先得”导师名额满了就自动拒绝。这里以志愿制为主讲因为它的状态更多写出来的代码覆盖了大多数场景的难点。双选涉及三张表select_apply存志愿teacher_quota存名额relation存最终确认的师生关系。提交志愿的接口是并发重灾区两个学生同时抢最后一个名额时如果不用锁数据库里会出现used_quota超出total_quota的脏数据。下面是提交志愿的完整代码Transactional(rollbackFor Exception.class) public void submitApply(StudentApplyDTO dto) { // 同一轮次只能提交一次志愿数据库里对 (student_id, round_no) 建了唯一索引 Integer existed applyMapper.countByStudentAndRound(dto.getStudentId(), dto.getRoundNo()); if (existed ! null existed 0) { throw new BizException(本轮志愿已提交不能重复提交); } // 先锁住导师名额这一行防止并发下两个学生同时抢最后一个名额 TeacherQuota quota teacherQuotaMapper.selectByTeacherIdForUpdate(dto.getTeacherId()); if (quota null || quota.getUsedQuota() quota.getTotalQuota()) { throw new BizException(该导师名额已满请选择其他导师); } // 学生当前不能已经存在“已确认”的导师关系 ListRelation confirmed relationMapper.findConfirmed(dto.getStudentId()); if (!confirmed.isEmpty()) { throw new BizException(你已经确认了导师不能继续填报志愿); } applyMapper.insert(new Apply() .setStudentId(dto.getStudentId()) .setTeacherId(dto.getTeacherId()) .setRoundNo(dto.getRoundNo()) .setPriority(dto.getPriority()) .setStatus(0)); // 0 待导师确认1 通过2 拒绝 }这段代码里最关键的是selectByTeacherIdForUpdate它在事务里对teacher_quota这一行加了悲观锁第二个事务执行到这里会被阻塞直到第一个事务提交。代价是这个接口在高并发下会排队但对研究生选导师这种场景一天也就几千次提交这个代价完全可接受。再用Transactional(rollbackFor Exception.class)把校验和插入放在同一个事务任何一个环节抛异常前面写进去的数据全部回滚不会出现“名额扣了但志愿没写进去”的半截状态。导师确认学生会走另一条路径查出所有待确认志愿按“志愿优先、时间优先”排序。SQL 如下SELECT ta.*, tq.total_quota, tq.used_quota FROM select_apply ta JOIN teacher_quota tq ON ta.teacher_id tq.teacher_id WHERE ta.status 0 -- 只取待确认 AND tq.used_quota tq.total_quota ORDER BY ta.priority ASC, -- 学生填的第一志愿排在前面 ta.create_time ASC -- 同优先级下先提交的排在前面 LIMIT 50;这个查询的意思是给导师展示一个“我还能确认谁”的列表优先展示第一志愿学生同志愿按提交时间排序。排序规则要和业务规则完全对齐否则会出现“第二志愿学生先被确认第一志愿学生反而被拒”的纠纷。确认动作是一个完整事务更新relation、更新select_apply.status、给teacher_quota.used_quota加一三步必须同时成功或同时失败。3.3 分页与条件查询LambdaQueryWrapper 的参数细节双选列表、学生列表、流程列表到处都是分页查询。MyBatis Plus 的分页插件配置很简单但参数设计有很多细节。先看配置Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 数据库类型要指定否则分页 count 语句可能用错方言 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }查询代码里我用 LambdaQueryWrapper 构造条件比写在 XML 里更安全——字段名写错在编译期就能发现GetMapping(/student/list) public ResultIPageStudentVO list(RequestParam(defaultValue 1) long current, RequestParam(defaultValue 10) long size, RequestParam(required false) String keyword, RequestParam(required false) String college) { PageStudent page new Page(current, size); LambdaQueryWrapperStudent wrapper new LambdaQueryWrapperStudent() .like(StringUtils.hasText(keyword), Student::getName, keyword) .eq(StringUtils.hasText(college), Student::getCollege, college) .orderByDesc(Student::getCreateTime); IPageStudent result studentMapper.selectPage(page, wrapper); return Result.ok(convertToVO(result)); // 不要直接把实体吐给前端 }like和eq的第一个参数是布尔条件StringUtils.hasText在 keyword 为空时会自动忽略该条件orderByDesc按创建时间倒序保证新提交的学生排在前面。分页请求里有三个参数容易被写错current从 1 开始而不是 0size要限制上限比如size 100时强制设为 100否则有人传一个size99999等于把全表拉出来最后Page 里的total是 long 类型前端做表格分页时要留意精度问题数据量大了之后可能溢出。4. 论文全流程状态机把开题、中期、答辩收进同一个审批服务论文流程是研究生管理系统里最有业务深度的地方。学生提交开题报告导师审提交中期报告导师审提交答辩申请导师审、教务再审。很多系统的做法是给每个类型写一套接口流程之间互相独立结果就出现“开题没通过也能提交答辩申请”这种漏洞。这章讲怎么用状态机把流程统一起来。4.1 为什么用状态字段而不是一摞布尔值最容易犯的错误是用布尔值表达流程状态opening_passed、midterm_passed、defense_passed各占一列。表面看很直观但实际用起来很难受第一你无法表达“提交了但还没审”这种中间态第二你想加一个“被驳回重新修改”的状态时要么加一列要么重新定义布尔含义破坏历史数据第三也是最要命的代码里没法做非法跳转校验因为布尔值之间没有顺序关系。所以这里用一个status字段表达单个流程的状态再用type区分流程类型。常见状态定义如下type状态流转thesis_topic选题0 草稿 → 1 待审核 → 2 通过 / 3 驳回 → 1 重新提交opening开题0 草稿 → 1 待审核 → 2 通过 / 3 驳回 → 1 重新提交midterm中期0 草稿 → 1 待审核 → 2 通过 / 3 驳回 → 1 重新提交defense答辩0 草稿 → 1 待审核 → 2 通过 / 3 驳回 → 1 重新提交但在真正落地时“选题通过”是“开题可提交”的前置条件“开题通过”是“中期可提交”的前置条件。这套前置依赖不属于状态机本身而是属于“流程编排”需要在业务层单独校验。把这两件事分开想代码就不会糊成一团。4.2 可复用的流程审批服务一个接口处理所有类型的审核用一张表、一个状态机管四种流程核心是建立一个“当前状态 → 允许流转到的状态”的映射表。审核接口拿到流程 ID 和目标状态先判断这个跳转合不合法再更新状态、写审核意见、记日志。代码如下Component public class PaperProcessService extends ServiceImplPaperProcessMapper, PaperProcess { /** key: 当前状态, value: 允许流转到的目标状态 */ private static final MapInteger, SetInteger TRANSITIONS new HashMap(); static { // 0 草稿 - 1 待审核学生提交 TRANSITIONS.put(0, Set.of(1)); // 1 待审核 - 2 通过 / 3 驳回 TRANSITIONS.put(1, Set.of(2, 3)); // 3 驳回 - 1 待审核修改后重新提交 TRANSITIONS.put(3, Set.of(1)); } Transactional(rollbackFor Exception.class) public void audit(Long processId, Integer targetStatus, String comment, String operatorRole) { PaperProcess process getById(processId); if (process null) { throw new BizException(流程记录不存在); } if (process.getStatus() 2) { throw new BizException(流程已通过不能重复审核); } // 状态机校验当前状态不允许流向目标状态时直接拒绝 SetInteger allowed TRANSITIONS.getOrDefault(process.getStatus(), Collections.emptySet()); if (!allowed.contains(targetStatus)) { throw new BizException(当前状态不允许流转到目标状态); } // 角色校验只有导师和管理员可以审核 if (!admin.equals(operatorRole) !teacher.equals(operatorRole)) { throw new BizException(该角色无权审核); } if (!StringUtils.hasText(comment)) { throw new BizException(审核意见不能为空); } process.setStatus(targetStatus); process.setAuditComment(comment); process.setAuditTime(new Date()); updateById(process); // 操作留痕答辩评审有争议时这是唯一的后悔药 auditLogMapper.insert(new AuditLog(processId, operatorRole, comment)); } }这段代码的逻辑分四层状态机校验、角色校验、数据更新、日志记录。TRANSITIONS用静态 Map 定义比 switch-case 好维护以后要加“撤回”状态只需要在对应的集合里加一个目标状态。updateById走 MyBatis Plus 的乐观锁version字段会自动加一两个审核人同时点“通过”时后提交的人更新影响行数为 0业务层再报“已被其他审核人处理”避免覆盖。这里还有一个隐藏设计前置流程校验不放在audit里而是放在“学生提交流程”的接口里。比如提交开题时检查选题流程状态为 2提交中期时检查开题状态为 2。为什么拆开因为审核动作只需要关心状态合不合法而提交动作才需要关心“你够不够资格提交”。把职责拆开后续新增“预答辩”流程时只需要改提交校验不需要动审核逻辑。4.3 状态回退、撤回与超时提醒毕业设计里学生提交错了文档、导师驳回了流程这些场景都需要状态回退。回退不是改个数字那么简单学生端要能退回草稿重新编辑导师端要能撤回已通过的审核有些学校允许在段时间内反悔管理员要能强制纠正异常状态。我的做法是给这个入口单独开接口并且只允许“高一级角色”操作学生能撤回“待审核”的流程导师能驳回“待审核”的流程管理员能强制重置任何流程。这个分级杜绝了“学生把自己答辩状态改成已通过”这种事故。超时提醒用 Spring 自带的Scheduled就够Scheduled(cron 0 0 8 * * *) public void remindPendingProcess() { // 超过 48 小时还没审核的流程汇总后发站内信 Date deadline new Date(System.currentTimeMillis() - 48 * 3600 * 1000L); ListPaperProcess pending baseMapper.selectList(new LambdaQueryWrapperPaperProcess() .eq(PaperProcess::getStatus, 1) .lt(PaperProcess::getSubmitTime, deadline)); for (PaperProcess process : pending) { messageService.sendToStudent(process.getStudentId(), 你的 process.getType() 流程等待审核已超过48小时); } }这个定时任务每天上午 8 点跑一次把超过 48 小时没审核的流程找出来通知学生。注意一个问题如果生产环境有多个后端实例Scheduled会在每个实例上都执行一遍需要引入分布式锁比如 Redisson保证同一时刻只有一个实例在跑。毕设系统单机部署没这个问题但你要知道边界在哪。5. 避坑手册研究生管理系统里最常见的五个翻车现场这一章写的都是实际改代码时反复遇到过的坑。每条按“现象 → 原因 → 解决”的顺序讲可以直接对应到你的报错日志和异常行为上。5.1 登录后所有接口全部 401现象用户输入正确的账号密码登录接口返回 token但拿着 token 访问任何业务接口都提示 401F12 里看到大量 OPTIONS 请求报错。原因基本出在两个地方。第一是拦截器没有放行跨域预检请求浏览器在发起 POST/PUT 前会先发一个 OPTIONS 请求这个请求不带 token如果拦截器直接拦截并返回 401后续业务请求根本发不出去。第二是 token 解析时用的 key 不一致比如登录时用secret-key签名拦截器里用另一个字符串解析能验签成功才见鬼。解决拦截器里对OPTIONS请求直接放行Spring Boot 的 CORS 配置和拦截器配置要同时存在缺一个都会有问题。token 的密钥统一放到application.yml的配置项里不要散落在代码各处。配置如下jwt: secret: your-secret-key expire-hours: 2这样登录和拦截器都从同一个配置源读取从根上消除不一致。5.2 导师双选出现一个学生两个导师现象双选结束后导出的师生关系表里同一个学生出现在两个导师名下管理员手动删都删不干净。原因确认接口没有做“学生已存在导师关系”的并发防护。两个导师同时点“确认”按钮两个事务都查了一遍relationMapper.findConfirmed(dto.getStudentId())都发现为空然后都插入成功。本质上是经典的“检查再插入”并发问题。解决第一道防线是数据库唯一索引在relation表的student_id字段上加唯一约束第二个插入直接被数据库拒绝这是兜底第二道防线是业务层校验用SELECT ... FOR UPDATE锁住学生的关系记录再检查第三道防线是提交前在select_apply表上把该学生所有已确认的志愿状态更新掉只允许一条流转。并发扣名额、重复确认这类场景现在几乎成了 java 八股文级别的经典考点面试问到的概率不低。5.3 论文流程能跳过开题直接到答辩现象学生没提交过开题报告系统里却能建出一条状态为“待审核”的答辩申请记录更离谱的是开题被驳回后中期记录照样能提交。原因每条流程都只校验自己的状态没做流程间的前置依赖校验。学生提交中期报告时代码只判断“中期流程当前是草稿”没有去查开题流程是否为“已通过”。解决在“提交流程”的接口里根据type做前置判断。提交 opening 时查 thesis_topic 状态提交 midterm 时查 opening 状态提交 defense 时查 midterm 状态。把这段判断抽成一个方法单独写单元测试覆盖“前置未通过”“前置已通过”“前置被驳回”三种情况。流程之间的依赖关系是业务规则不能指望前端按钮隐藏来保证安全。5.4 答辩成绩计算出现空指针与精度丢失现象答辩录入分数时某些学生成绩记不上平均分算出来是整数84.5 变成 84评委只打了一组分数的学生会整条记录失败。原因一是数据库中成绩列允许为空service 层直接用Integer累加空值一参与计算就 NPE二是用了int做除法小数部分被截断三是某个评委没打分时ListScore集合里存在 null 元素。解决成绩字段统一用BigDecimal数据库列用DECIMAL(5,2)并在 DDL 里加NOT NULL DEFAULT 0把空值挡在入口之外计算平均分时用BigDecimal的divide方法指定保留两位小数评委分数集合在遍历前过滤掉 null。核心原则是分数计算不信任数据库的宽松能加默认值就加默认值能在 DDL 层卡住的事情不要让代码去兜底。5.5 导入学生名单时中文乱码与学号变科学计数法现象用 EasyExcel 或 POI 导入学生 Excel学号变成了1.23457E10中文姓名显示成乱码日期字段读出序列号。原因Excel 里数字单元格默认被读成 double15 位以上学号精度丢失编码问题多发生在读取 xls 老格式时用了错误的字符集日期列读出来是整数序号需要手动转日期格式。解决用 POI 的DataFormatter读取单元格原始展示样式不要直接用getNumericCellValue// 按单元格的展示格式读值学号、日期都不会丢格式 DataFormatter formatter new DataFormatter(); String cellValue formatter.formatCellValue(cell);它的作用是“单元格在 Excel 里显示成什么样就读成什么样”学号不会变科学计数法日期也不会变序列号。文件流读取时统一用xlsx格式保存xlsx本身就是 UTF-8 编码的 zip比老的xls少了大量编码坑。如果条件允许直接让学生用系统提供的模板下载后填写再上传把“列的标题和顺序”这个问题从源头解决。6. 验证与进阶用一条命令把系统跑起来并走通全流程系统写完不是终点能演示、能让人相信“这是能用的系统”才是终点。最常见的遗憾是管理员、导师、学生三个账号都有但演示时只展示学生端菜单导师端和教务端的操作全靠口头描述。这里分享一个我一直在用的验证方法——写一段 bash 脚本把完整业务链路串起来演示的时候跑一遍比切换页面点鼠标更有说服力也比网上那些课程设计案例源码更进一步。脚本的核心思路是模拟一条完整链路学生登录 → 提交开题 → 导师登录 → 审核通过 → 学生提交中期 → 导师审核 → 学生提交答辩 → 管理员查看进度BASEhttp://localhost:8080/api # 1. 学生登录拿 token TOKEN$(curl -s -X POST $BASE/auth/login \ -H Content-Type: application/json \ -d {username:2023001,password:stu123456} | jq -r .data.token) # 2. 学生提交开题流程 curl -s -X POST $BASE/paper/process \ -H Authorization: Bearer $TOKEN \ -H Content-Type: application/json \ -d {type:opening,title:基于Spring Boot的研究生管理系统设计与实现} # 3. 导师登录审核通过 T_TOKEN$(curl -s -X POST $BASE/auth/login \ -H Content-Type: application/json \ -d {username:t2001,password:tea123456} | jq -r .data.token) curl -s -X POST $BASE/paper/process/audit \ -H Authorization: Bearer $T_TOKEN \ -H Content-Type: application/json \ -d {processId:1,targetStatus:2,comment:同意开题}这段脚本依赖curl和jqjq用从 JSON 里取 token没有的话可以用 Python 的json.tool替代或者直接打印返回体手抄 token。要注意的是脚本里的 ID 和 username 是写死的跑之前先确认数据库里的种子数据存在。建议在schema.sql里预置 5 个学生、3 个导师、2 个管理员密码统一设为公开默认值并在演示环境关闭密码复杂度校验。验收时我还习惯对照一张清单学生能否看到自己的全部审核进度导师能否只看到名下学生管理员能否强制撤回异常状态两个浏览器同时操作同一流程会不会出现重复通过。这四件事都过了系统的核心可信度就立住了。最后说一个我自己的教训。最早做这类系统时我把审核通过写成了直接改status前端按钮一多演示现场点错了把答辩状态从“待审核”点成了“已通过”当场数据就乱了。后来全部改成后端状态机校验前端按钮只是调接口代码层面才真正兜住了底线。希望这篇笔记能让你少走这段弯路也希望你做完之后敢在任何人面前打开演示页面跑完整条链路。本文还有配套的精品资源点击获取