
简介一份基于Java的本科生实习管理系统的设计与实现毕业设计文档面向计算机相关专业学生、毕业设计选题者以及对JSP与MySQL开发感兴趣的学习者。文档完整呈现了基于B/S架构、采用JSP与MySQL数据库开发的实习管理系统的全流程内容涵盖研究背景、可行性分析与需求分析、系统总体框架设计、数据库设计以及用户登录、修改密码、学生上传文件、教师管理学生、管理员管理教师等核心模块的详细实现并包含系统测试目标、方法与具体用例结果。全文共15页具备摘要、Abstract、目录、绪论、参考文献、致谢等完整结构既可作为课程设计与毕业设计论文的写作模板也能帮助读者梳理JSP与MySQL Web应用的实际开发思路。压缩包内仅有1个docx文档大小约720KB全部为可直接查阅编辑的论文正文。已有422人学习适合需要快速获取实习管理系统设计思路或撰写同类论文的读者。1. 为什么“基于Java的本科生实习管理系统的设计与实现”是毕设热点也是最容易做成一堆CRUD的题目基于Java的本科生实习管理系统的设计与实现这个标题在每年的Java类毕设清单里几乎是固定嘉宾。我看过不少拿这个题目的同学以为把学生表、老师表、实习记录表各建一张再做几个增删改查页面就能交差。实际上这个系统最值钱的部分不是CRUD而是管理实习申请怎么流转、不同角色看到什么、状态怎么推进、老师怎么给学生评分、学院怎么统计数据。这篇笔记按我做过的方案讲一遍从需求建模到Spring Boot Vue落地再到那些会让答辩现场翻车的细节适合正在复现这套系统的同学。你最后交的虽然是一个.docx文档但文档背后必须有一个能跑起来、能演示、能回答提问的系统这套工程思路会比一句“用SSM实现了增删改查”值钱得多。2. 先把业务边界画清楚四类用户、六种状态、三条核心流程2.1 用户角色与权限学生、校内导师、校外导师、院系管理员怎么共用一个登录表很多人的第一版设计是四种表学生表、老师表、学院表、实习单位表。学生和老师各自注册登录时判断去查哪张表。结果就是角色一多登录逻辑变成if-else的泥潭后面加一个校外导师又要改登录。我一般会做一张统一的用户表再加上角色表或直接在用户表里存角色枚举。用户表的核心字段id、username、passwordBCrypt加密、real_name、role_type枚举定死STUDENT/INTERNAL_TEACHER/EXTERNAL_TEACHER/ADMIN、phone、email、status1启用0禁用。注意role_type不要用字符串学生老师这样后面加角色或做权限判断都难维护。用枚举字符串或数字都行我推荐用字符串枚举可读性好而且前端能直接拿来判断。权限这块如果做简单一点不需要Spring Security那套复杂的过滤器链只需要一个自定义拦截器根据请求路径前缀判断角色。例如/api/student/**只允许STUDENT角色/api/manage/**只允许ADMIN。但如果你想把按钮级权限、菜单权限做细建议引入Spring Security JWT后面第4章会讲到。这里把数据模型先定住。2.2 实习流程状态机从“待申请”到“已归档”的六个状态实习管理系统如果只有开始实习结束实习那确实很简单。但学院通常要求学生先申请导师审批审批通过后填写实习计划实习过程中交周报或签到结束再交实习报告导师评分学院管理员归档。把这些串起来就是一个状态机。我建议在代码里用一个枚举类InternshipStatus而不是在数据库里直接散落多个status字段。六个状态PENDING_APPLY待提交申请、PENDING_REVIEW待导师审批、IN_PROGRESS实习中、PENDING_REPORT待提交报告、COMPLETED已评分、ARCHIVED已归档。状态流转要画成有向图学生可以提交申请、撤回、提交报告导师只能审核、评分管理员只能归档。这里最容易错的是撤销很多同学只做正向流转没做反向。实际业务上学生提交申请后在导师未审核前应当能撤回否则填错了单位名只能干等。public enum InternshipStatus { PENDING_APPLY(待提交申请), PENDING_REVIEW(待导师审批), IN_PROGRESS(实习中), PENDING_REPORT(待提交报告), COMPLETED(已评分), ARCHIVED(已归档); private final String desc; InternshipStatus(String desc) { this.desc desc; } public String getDesc() { return desc; } }状态流转建议在Service层做校验不要只在前端控制。比如前端隐藏了按钮但有人直接调接口把状态从待申请改成已评分怎么办后端必须判断当前状态是否等于允许流转的前置状态否则返回业务错误码。比如submitApply()只允许从PENDING_APPLY流转到PENDING_REVIEWapprove()只允许从PENDING_REVIEW流转到IN_PROGRESS。这个设计在答辩时被问到“如果学生跳过审核直接提交报告怎么办”就有现成的答案。2.3 数据库表设计用建表SQL把设计具象化以下是核心四张表的MySQL建表脚本。注意我跳过了user_role、menu等辅助表只看实习业务主链路。-- 用户表学生、校内导师、校外导师、管理员统一存在这里 CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT BCrypt密文, real_name VARCHAR(32) NOT NULL, role_type VARCHAR(16) NOT NULL COMMENT STUDENT/INTERNAL_TEACHER/EXTERNAL_TEACHER/ADMIN, phone VARCHAR(20), email VARCHAR(64), status TINYINT DEFAULT 1 COMMENT 1启用 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 实习单位表 CREATE TABLE internship_unit ( id INT PRIMARY KEY AUTO_INCREMENT, unit_name VARCHAR(128) NOT NULL, unit_address VARCHAR(255), unit_type VARCHAR(32) COMMENT 国企/外企/民营/事业单位, contact_person VARCHAR(32), contact_phone VARCHAR(20), status TINYINT DEFAULT 1 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT实习单位表; -- 实习记录主表 CREATE TABLE internship_record ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, unit_id INT NOT NULL, internship_start DATE NOT NULL, internship_end DATE NOT NULL, job_title VARCHAR(64) COMMENT 实习岗位, status VARCHAR(20) NOT NULL DEFAULT PENDING_APPLY, score DECIMAL(5,2) DEFAULT NULL COMMENT 导师评分保留两位小数, report_path VARCHAR(255) COMMENT 实习报告文件存储路径, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_student_status (student_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT实习记录主表; -- 审批日志表每次状态流转都留一条记录 CREATE TABLE internship_audit ( id INT PRIMARY KEY AUTO_INCREMENT, record_id INT NOT NULL, from_status VARCHAR(20), to_status VARCHAR(20), operator_id INT NOT NULL, remark VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_record_id (record_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT审批日志表;几个关键点记录主表里加联合索引(student_id, status)因为这个系统最常见的查询是某个学生当前有哪些状态的实习记录没有这个索引数据量一上百条慢查询就出来了。审批日志表是后来补的我见过很多同学不做这张表结果答辩被问你如何审计某个实习记录的历史操作答不上来。其实一张日志表就能讲清楚。status我用VARCHAR存枚举名而不是数字因为数字0/1/2在代码里还要映射枚举名在日志和调试里一眼就看懂。缺点是稍微占一点存储但对这种规模的系统完全可以忽略。score用DECIMAL(5,2)别用FLOAT浮点比较在MySQL里容易出错。日期用DATE时间用DATETIME不要Timestamp混用时区问题到避坑章节再细说。另外实习申请时学生填的表单实际上我通常把申请表字段直接放到internship_record里而不是单独建application表。因为申请被批准后实习就开始主记录的行就是那条实习记录申请日期、实习日期都在同一行状态字段从PENDING_APPLY开始流转。如果拆成两张表就要做关联状态同步容易出现一边改一边没改的问题。对于这种规模的教学型系统单表状态机比主从表更省心。文件存储方面实习报告、盖章证明、周报这些我建议在服务器上按/upload/record/{recordId}/目录放文件数据库只存相对路径。不要用BLOB直接把文件塞进MySQL数据库虽然课程设计里这么做看着方便但数据库会越撑越大备份和迁移都变慢。3. 用 Spring Boot MyBatis 把后端接口立起来关键代码与参数3.1 项目骨架与配置文件pom.xml 和 application.yml 里的三个必调参数常见做法是用Spring Boot 2.7.xJava 8。Spring Boot 3也可以但要求Java 17很多学校电脑还是JDK 8为了答辩现场不翻车我一般选SpringBoot 2.7 JDK8。pom.xml核心依赖就是web、mybatis、mysql、pagehelper、jwt这几个。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.auth0/groupId artifactIdjava-jwt/artifactId version4.4.0/version /dependency /dependencies版本号都是目前网上大量教程在用的不会踩兼容性的坑。这里用原生MyBatis PageHelper而不是MyBatis-Plus是因为答辩时被问到SQL映射和分页原理你能答得更清楚。如果不想写XML用MyBatis-Plus也行但下面的示例要换成BaseMapper风格。application.yml里三个必调参数spring: datasource: url: jdbc:mysql://localhost:3306/internship_db?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 20MB max-request-size: 50MB jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.internship.entity configuration: map-underscore-to-camel-case: true pagehelper: helper-dialect: mysql reasonable: true support-methods-arguments: true三个必调参数第一是serverTimezoneAsia/Shanghai不加这个MySQL 8连接报时区错误或日期差8小时。第二是max-file-size和max-request-sizeSpring Boot默认只有1MB实习报告PDF分分钟超限。第三是map-underscore-to-camel-case数据库real_name才能正确映射到Java的realName字段不然每次都要写Results注解。PageHelper的reasonable参数也建议打开意思是页码小于1时返回第一页大于总页数时返回最后一页而不是直接报错。3.2 带分页和状态过滤的实习记录列表接口控制层、服务层、Mapper三层怎么写先定义接收参数的DTO。为了示例简洁省略getter/setter。public class InternshipQueryDTO { private Integer pageNum 1; private Integer pageSize 10; private String status; // 状态枚举名可为空 private Integer studentId; // 可为空管理员查询时用 private String keyword; // 单位名称模糊搜索 // getter/setter 略 }Controller层RestController RequestMapping(/api/records) public class InternshipRecordController { Autowired private InternshipRecordService recordService; GetMapping public ResultPageInfoInternshipRecordVO list(InternshipQueryDTO query) { if (query null) { query new InternshipQueryDTO(); } query.setPageNum(Math.max(query.getPageNum(), 1)); query.setPageSize(Math.min(query.getPageSize(), 100)); PageInfoInternshipRecordVO page recordService.pageQuery(query); return Result.success(page); } }注意这里我用GetMapping加普通参数绑定前端传?pageNum1pageSize10statusIN_PROGRESS就行。如果查询条件特别多改用POST配RequestBody但接口风格要和前端约定好别混着用。Service层Service public class InternshipRecordService { Autowired private InternshipRecordMapper recordMapper; public PageInfoInternshipRecordVO pageQuery(InternshipQueryDTO query) { PageHelper.startPage(query.getPageNum(), query.getPageSize()); ListInternshipRecordVO list recordMapper.selectPage(query); return new PageInfo(list); } }PageHelper.startPage必须放在调用Mapper方法的前一行中间不能有其他查询否则分页插件会作用到别的查询接口上。这是PageHelper最经典的坑。Mapper接口public interface InternshipRecordMapper { ListInternshipRecordVO selectPage(InternshipQueryDTO query); }Mapper XMLselect idselectPage resultTypecom.example.internship.vo.InternshipRecordVO SELECT r.id, u.real_name AS studentName, iu.unit_name, r.internship_start, r.internship_end, r.job_title, r.status, r.score FROM internship_record r JOIN sys_user u ON r.student_id u.id JOIN internship_unit iu ON r.unit_id iu.id where if teststatus ! null and status ! AND r.status #{status} /if if teststudentId ! null AND r.student_id #{studentId} /if if testkeyword ! null and keyword ! AND iu.unit_name LIKE CONCAT(%, #{keyword}, %) /if /where ORDER BY r.create_time DESC /select这个查询把用户表的真实姓名和单位表名称join进来避免在VO里再拼一次额外查询。where标签会自动去掉最前面的AND这个细节在动态SQL里非常常见。分页插件返回的PageInfo里包含total、pageNum、pageSize、list等字段前端可以直接用。注意查完数据后PageInfo要立刻构造不要做其他查询否则分页信息可能不准确。3.3 文件上传的存储策略本地磁盘 vs 对象存储参数怎么设实习报告、实习证明图片、周报文件都需要上传。如果只做课程设计本地磁盘存储就够了。配置两个项上传根目录和静态访问前缀。app: upload-dir: /data/internship-upload upload-url-prefix: /upload/上传接口RestController RequestMapping(/api/upload) public class UploadController { Value(${app.upload-dir}) private String uploadDir; PostMapping(/report) public ResultString uploadReport(RequestParam(file) MultipartFile file, RequestParam(recordId) Integer recordId) { // 校验文件类型只允许pdf、doc、docx、jpg、png String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.) 1).toLowerCase(); ListString allowed Arrays.asList(pdf, doc, docx, jpg, png); if (!allowed.contains(ext)) { return Result.error(不支持的附件类型); } // 校验文件大小虽然spring层已限制20MB但再查一次更保险 if (file.getSize() 20 * 1024 * 1024) { return Result.error(文件不能超过20MB); } // 用recordId和UUID组织路径防止文件名重复 String relativePath record/ recordId / UUID.randomUUID() . ext; File dest new File(uploadDir, relativePath); if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); } try { file.transferTo(dest); } catch (IOException e) { return Result.error(文件保存失败); } return Result.success(relativePath); } }这里有个血泪经验MultipartFile.transferTo在不同系统上处理绝对路径和相对路径的行为不一样建议先把完整的File路径构造好再调用。另外一定要先创建父目录否则会抛FileNotFoundException。静态资源映射Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file:/data/internship-upload/); } }注意file:前缀必须是绝对路径且结尾要有/。这里如果配置错了前端能返回记录路径但图片/PDF就是访问不了浏览器404。这是另一个高频翻车点。对象存储OSS/MinIO适合真实部署但课程设计答辩时讲清楚本地磁盘方案足够。如果想加分可以说明你把文件存储路径设计为可配置未来切换MinIO只需要改配置和实现类。4. 前端用 Vue Element UI 接上后端登录、路由守卫、按钮权限4.1 登录接口与Token刷新前端把用户信息挂到store前端技术选型我建议Vue 2 Element UI因为网上资料最多答辩时临时改样式也容易。现在Spring Boot Vue这套组合在毕设和智慧校园类系统里几乎成了默认配置老师也认可。登录流程前端调后端/api/auth/login拿到JWT和用户信息存到localStorage和Vuex。其中一个重要经验不要把用户密码存到localStorage只存token和non-sensitive信息例如realName、roleType。async function login(username, password) { const { data } await axios.post(/api/auth/login, { username, password }); if (data.code 200) { localStorage.setItem(token, data.data.token); localStorage.setItem(userInfo, JSON.stringify(data.data.userInfo)); store.dispatch(setUser, data.data.userInfo); router.push(/dashboard); } }后端登录ControllerPostMapping(/api/auth/login) public ResultLoginVO login(RequestBody LoginDTO dto) { SysUser user userMapper.findByUsername(dto.getUsername()); if (user null || !BCrypt.checkpw(dto.getPassword(), user.getPassword())) { return Result.error(用户名或密码错误); } if (user.getStatus() 0) { return Result.error(账号已被禁用); } String token JWT.create() .withClaim(userId, user.getId()) .withClaim(roleType, user.getRoleType()) .withExpiresAt(new Date(System.currentTimeMillis() 8 * 3600 * 1000L)) .sign(Algorithm.HMAC256(your-secret-key)); LoginVO vo new LoginVO(); vo.setToken(token); vo.setUserInfo(user); return Result.success(vo); }这里密码用BCryptspring-security-crypto提供不要用MD5。用MD5存密码在专业答辩里减分很严重。一个常见问题是JWT有效期设多长课程设计我建议8小时足够一天操作。真实系统用2小时加刷新Token。如果你不想实现刷新逻辑就把过期时间设到24小时但要在前端拦截401后跳回登录页。4.2 路由守卫未登录跳转、已登录但角色不对时显示403Vue Router的路由守卫是前端权限控制的关键。把需要鉴权的路由配置meta.rolesconst router new VueRouter({ routes: [ { path: /login, component: Login }, { path: /records, component: RecordList, meta: { roles: [STUDENT, INTERNAL_TEACHER, ADMIN] } }, { path: /students, component: StudentManage, meta: { roles: [ADMIN] } }, // 其他路由... ] }); router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path /login) { next(); return; } if (!token) { next(/login); return; } const userInfo JSON.parse(localStorage.getItem(userInfo) || {}); if (to.meta.roles !to.meta.roles.includes(userInfo.roleType)) { next(/403); return; } next(); });注意这里把角色判断写在前端路由守卫里只是用户体验真正的权限拦截还是要靠后端接口的拦截器。如果后端不加拦截器别人直接POST/api/students就能拿到数据前端守卫形同虚设。拦截器写法public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { response.setStatus(401); return false; } try { DecodedJWT jwt JWT.require(Algorithm.HMAC256(your-secret-key)) .build() .verify(token.replace(Bearer , )); request.setAttribute(userId, jwt.getClaim(userId).asInt()); request.setAttribute(roleType, jwt.getClaim(roleType).asString()); return true; } catch (JWTVerificationException e) { response.setStatus(401); return false; } } }这里有个细节前端请求带Authorization头会触发跨域预检所以拦截器要放行OPTIONS否则浏览器请求一直失败。另外记得在WebMvcConfig里注册拦截器并排除/api/auth/login和/upload/**否则静态资源也会被拦。4.3 页面组件实习生信息维护页怎样做到“增删改查一张表”用Element UI的el-table加对话框是Vue后台管理最经典的模式。核心代码template div el-table :datatableData v-loadingloading el-table-column propstudentName label姓名/el-table-column el-table-column propunitName label实习单位/el-table-column el-table-column propstatus label状态 template slot-scopescope el-tag :typestatusTag[scope.row.status]{{ statusText[scope.row.status] }}/el-tag /template /el-table-column el-table-column label操作 template slot-scopescope el-button v-ifcanReview sizemini typeprimary clickreview(scope.row)审核/el-button /template /el-table-column /el-table el-pagination background layouttotal, prev, pager, next :totaltotal :current-page.synccurrentPage :page-size.syncpageSize current-changefetchData /el-pagination /div /template script export default { data() { return { tableData: [], loading: false, currentPage: 1, pageSize: 10, total: 0, canReview: this.$store.state.userInfo.roleType INTERNAL_TEACHER }; }, created() { this.fetchData(); }, methods: { async fetchData() { this.loading true; const { data } await axios.get(/api/records, { params: { pageNum: this.currentPage, pageSize: this.pageSize } }); this.tableData data.data.list; this.total data.data.total; this.loading false; } } }; /script注意canReview在data里初始化一次如果同一个页面里用户角色会动态变化应改成计算属性。这个例子只是展示按钮级控制的思路真正的系统要把权限封装成全局指令。像v-permissionINTERNAL_TEACHER这样。跨浏览器支持上Vue 2 Element UI 对Chrome、Firefox、Edge都没问题但对IE11需要polyfill。如果你不确定答辩机器是什么浏览器建议提前把演示环境固定成Chrome或者在真机装一个绿色版Chrome。这东西表面上是小事但现场白屏一次后面全慌了。5. 避坑答辩前最容易翻车的 5 个 Java Web 细节5.1 数据库时间字段到处是“2025-12-25 08:00:00”前端却是“NaN-NaN-NaN”现象数据库里存的时间是正确的但后端返回给前端的时间字符串变成了前一天或者前端显示“NaN-NaN-NaN”。原因三处时区不一致——MySQL连接串没加serverTimezoneJVM使用的系统时区可能是UTCSpring Boot Jackson默认序列化没有指定格式。解决在连接串加serverTimezoneAsia/Shanghaiapplication.yml配jackson.time-zone和date-format前端用dayjs统一格式化。另外一个建议数据库字段类型不要用timestamp它在MySQL里会受全局时区影响date/datetime相对安全。这问题在答辩现场出现得特别多因为换一台电脑JVM时区可能就变了。5.2 文件上传大小限制传个实习报告PDF都报MaxUploadSizeExceededException现象点击上传实习报告按钮转几圈然后弹“请求被拒”控制台报MaxUploadSizeExceededException。原因Spring Boot内置的multipart默认单个文件最大1MB很多同学只配置了max-file-size但没配max-request-size表单里同时有文件和参数时请求体整体超过限制也会报错。解决在application.yml里同时配置max-file-size: 20MB和max-request-size: 50MB并在Controller加ExceptionHandler(MaxUploadSizeExceededException.class)返回友好提示。注意单位是MB大小写写错了也解析不出来。这条属于最常见的“配置了但没生效”之一。5.3 状态流转没有加乐观锁学生和导师同时点击数据被覆盖现象学生在实习中导师同时在审核两个操作都基于旧状态后写的覆盖先写的状态直接跳错。原因没有做并发控制。解决在internship_record表加version字段更新时在SQL里写where id #{id} and version #{version}同时自增version。代码里捕获AffectedRows0返回“数据已被他人修改请刷新”。这比简单加synchronized更像懂工程的处理答辩时能加不少印象分。5.4 PageHelper分页参数不校验页码越界后返回空列表用户以为没数据现象页码传0或-1返回空列表传999也返回空列表前端看不出问题但用户以为没数据。原因没开reasonable且没做参数校验。解决在application.yml开reasonable: true让PageHelper自动把页码归一化同时Service层再用Math.max兜底。注意reasonable对pageSize也生效pageSize0时PageHelper可能会执行全表查询必须在DTO层限制pageSize范围在1到100之间。这条是分页插件的老坑但每年都有人中招。5.5 Java启动失败怎么解决端口被占用、版本不匹配、依赖冲突三个高频原因现象双击启动控制台一片红。原因通常是三个端口被占用、Maven依赖下载不完整、JDK版本不对。解决先看堆栈如果是Port in use用netstat -ano | findstr 8080找到占用进程或者直接换端口启动java -jar xxx.jar --server.port8081。如果是依赖冲突用mvn dependency:tree查看依赖树重点看spring-boot-starter-parent和mybatis-spring-boot-starter的版本兼容性。如果是版本问题确认JDK 8不能跑Spring Boot 3。提前在README里写好启动步骤并在答辩前一天拿一台“干净”电脑没有配过环境的试一遍。很多同学在答辩现场第一次启动项目报错就慌这不是技术问题是流程问题。6. 进阶把“课程作业”升级成“可答辩工程”的三个技巧6.1 用统计接口给学院展示实习去向加分项是做一个统计接口按单位类型聚合实习人数。SQL一行就好SELECT unit_type, COUNT(*) AS cnt FROM internship_unit iu JOIN internship_record r ON iu.id r.unit_id GROUP BY unit_type前端用ECharts画饼图或柱状图。这个功能能回答老师“这个系统除了CRUD还有什么价值”的问题——它确实能给学院管理提供决策数据。6.2 用AOP统一记录操作日志在关键Service方法上加自定义注解OpLog(提交实习申请)然后用AOP切面记录操作人、操作时间、方法名和参数。这段代码不复杂但能体现工程分层意识。注意别记录密码等敏感字段参数里如果有HttpServletRequest先过滤。6.3 用JMeter验证200并发下的接口表现答辩前花半小时跑一个JMeter冒烟测试能让你回答“系统能扛多大并发”。新建一个线程组设200线程循环10次请求/api/records列表接口然后看聚合报告里的吞吐量、错误率。哪怕只有几百的TPS也比没有数据强。注意测试前先把数据库连接池参数调一下Spring Boot默认的HikariCP最大连接数是10200并发时连接数不够会大量等待。spring.datasource.hikari.maximum-pool-size可以设到50但别太高免得把MySQL压垮。做完这三件事你在答辩时就能张嘴就说“这里我加了一个统计接口”“这里我做了操作日志审计”“这里我做过并发测试”而不是反复解释“这个按钮是增删改查”。这套做法我用了很多年最深的教训就是课程设计不是功能越多越好而是在每个细节上能讲清楚为什么。希望帮到你。本文还有配套的精品资源点击获取