做毕设选眼科患者随访管理系统的同学真的不少最近我也前前后后帮人看了好几套基于Java生态的这类项目。说实话这个题目选得很聪明——业务场景明确不算复杂到失控又能把增删改查、权限控制、定时任务、报表统计这些后端技术点全部串起来。今天就把我搭建基于SpringBootSSM这套技术栈做眼科患者随访管理系统时的设计思路、核心代码、数据库表结构和调试经验整个捋一遍给正在做毕设、准备答辩或者单纯想了解医疗随访系统怎么落地的人一个可以照着做的参考。1. 从业务痛点说起眼科随访到底在管理什么1.1 为什么要做眼科患者的随访管理眼科患者随访管理系统名字看着长落到地上就是一件事把患者治疗后需要复查的时间线用系统管起来到点提醒医生别漏掉任何一个该复查的人。眼科里需要长期随访的病种非常多。青光眼患者要定期测眼压白内障术后要复查视力和人工晶体位置糖尿病视网膜病变患者要按节点做眼底检查近视手术之后还要跟踪角膜恢复情况。这些都不是看一次门诊就能结束的病复查节点往往分布在术后第1天、第7天、第30天、第90天、半年、一年。要是靠医生脑子里记靠翻纸质病历本靠护士一个一个打电话问效率低不说漏掉一个人可能就是一次不可逆的视力损伤。所以医院科室里非常需要一套能自动算时间、自动提醒、留痕记录的随访管理工具。我接触这个项目时的第一判断是它不算一个高难度系统但它是一个特别典型的医疗信息管理系统。它覆盖了患者档案管理、随访计划安排、随访结果记录、统计报表这几块核心业务技术点上又把Java后端最常见的CRUD、权限控制、定时任务、数据导入导出全部串了起来。这正是每年毕业设计选这个题的人这么多的重要原因——难度适中技术点常规但业务逻辑能讲出东西来。1.2 系统角色和核心业务流程先说清楚系统里的几个角色。管理员负责系统配置、用户管理和基础数据维护医生或者随访人员负责录入患者、给患者安排随访计划、按计划执行随访并填写记录系统本身负责在关键时间点把“今天该随访谁”这件事拎出来。患者是这个业务的核心主体所有随访动作都围绕患者展开。从业务流程看核心链路是这样的患者录入建档医生根据手术方案或治疗阶段给患者设定随访周期系统自动计算并生成随访任务到时间提醒医生医生执行随访并填写结果系统再根据随访结果滚动生成下一次计划。整个流程走下来患者从建档到每一次复查之间的衔接都有据可查医生在后台可以看到所有待办随访、逾期随访和已完成随访数据上比纯手工记录可靠得多。当初梳理需求的时候我一直在强调“闭环”这两个字。第一次做这类系统不需要功能多花哨但至少要保证患者从入院建档到术后随访结束每一个状态都有地方存、都能被查到。这条主线一旦捋直了后面写代码、写论文、画图都会顺很多。2. 技术选型分析SpringBootSSM到底是套什么组合2.1 厘清概念SSM和SpringBoot并不冲突很多同学看到项目标题里的“JavaSpringBootSSM”会觉得有点重复——SpringBoot不是已经内置SpringMVC了吗再加一个SSM是不是多余这个疑问我在面试应届生和带毕设的时候遇到过很多次确实是普遍误区。这里要讲清楚。“SSM”指的是Spring SpringMVC MyBatis这套经典Java Web组合。传统SSM项目的搭建非常痛苦要手工写web.xml、spring-mvc.xml、mybatis-config.xml、数据源文件等等一大堆XML各框架版本还得小心翼翼对齐一个namespace配错整个项目就起不来。SpringBoot出现之后绝大多数新项目不再走手工XML配置那条路了。所以现在标着“SpringBootSSM”的项目实际含义是用SpringBoot做基础框架依赖它内置的SpringMVC处理Web请求再集成MyBatis做持久层。SpringBoot负责自动配置和依赖管理MyBatis继续沿用Mapper接口加XML写SQL的老规矩。说白了这个组合就是“SpringBoot的省心加上MyBatis的灵活”。对这类管理信息系统来说非常合适一是网上资料极多踩了坑一搜就有解决方案二是MyBatis的SQL都是自己写的答辩时老师问业务逻辑能讲得清楚三是SpringBoot内置Tomcat打包后java -jar直接运行部署演示都方便。2.2 项目架构和各层职责我实际采用的是一个典型的前后端半分离方案。页面用Thymeleaf模板引擎渲染配合Bootstrap做界面样式页面里的交互用jQuery发Ajax请求和后端通信。后端是标准三层架构Controller层负责接收请求、参数校验、返回视图或JSONService层处理具体业务逻辑Mapper层也就是DAO层负责数据库交互。实体类用Lombok简化getter和setter省掉大量样板代码。为什么不用前后端彻底分离的Vue加SpringBoot方案不是不行而是要看工作量。毕设答辩的评审重点通常在后端设计和业务逻辑上半分离方案可以把关注点集中在Java这一侧前端不用维护独立的构建工程和Node环境现场部署的时候少很多变数。如果你的项目是想要前端效果更炫那另说Vue加ElementUI做页面确实颜值更高但两手抓的前提是你得有时间调跨域、调打包、调接口联调。技术组件选择上我整理过一个对照表觉得还是挺清晰的技术组件本项目的选择选型理由基础框架SpringBoot自动配置、内置Tomcat、生态成熟Web层SpringMVCSpringBoot默认集成注解开发简单持久层MyBatisSQL可控XML动态SQL适合多条件组合查询模板引擎Thymeleaf与SpringBoot整合好语法直观数据库MySQL 8.0免费、部署简单、资料丰富前端UIBootstrap jQuery上手快组件够用图表ECharts开源免费统计图表效果好权限拦截器 Session角色少、粒度粗够用且好解释2.3 依赖清单和核心配置pom.xml里的核心依赖大概是下面这些。我特别想提醒版本对齐这件事如果SpringBoot是2.xmybatis-spring-boot-starter用2.2.x这一档没问题但如果是SpringBoot 3.x要对应mybatis-spring-boot-starter 3.0以上的版本因为SpringBoot 3基于Jakarta EE规范javax包名整个被换掉了。每年都有同学栽在版本对应关系上启动直接报ClassNotFoundException。dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.2.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version5.2.3/version /dependency /dependenciesapplication.yml配置如下。数据库连接串里的serverTimezoneAsia/Shanghai这一项看着不起眼缺了它就会出现数据库时间和系统时间对不上尤其当你用new Date()存时间字段的时候。server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/eye_follow_up?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 thymeleaf: cache: false mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.eyefollow.entity configuration: map-underscore-to-camel-case: true说说选MySQL而不是PostgreSQL或者Oracle的理由。毕设场景下资料量、部署成本、可视化工具友好度三个维度MySQL都是最优解。Navicat对不熟悉命令行的学生特别友好网上关于MySQL的优化方案、面试题一大堆出问题也容易排查。记住一个原则——选技术栈不是选最先进的是选你最能把控的。这话放到就业以后同样成立。3. 功能模块与数据库设计核心表结构这样定3.1 五个功能模块的划分我把系统拆成五个功能模块每个模块对应论文里的一个章节答辩时讲起来层次很清楚。第一个是系统管理模块。用户管理、角色管理、菜单显示控制、操作日志都归到这里。用户和角色我做成多对多关系用户表、角色表、中间表各一张。以后想扩展“护士长”“随访专员”这类角色只要往角色表和中间表插数据就行不用改代码逻辑。第二个是患者信息管理模块。患者建档、修改、查询、删除支持按姓名、病历号、电话、病种模糊查询列表分页展示。注意这个模块不是简单的新增和删除而是整个系统的数据地基后面的随访计划、随访记录、统计报表全都依赖这里的患者数据。第三个是随访计划管理模块。医生为患者创建随访计划设定随访周期类型系统按周期自动生成具体的随访任务。计划状态我设计了四种待执行、已完成、已逾期、已取消分别用不同颜色标识。第四个是随访记录管理模块。医生填写每一次随访的结果包括随访方式、患者主诉、视力情况、眼压情况、医嘱说明、是否安排下次随访等。关于“下次随访”我用的不是一个布尔值而是一个“下次随访间隔天数”的数字字段填了之后系统自动生成下一条计划。这样就把一次随访和整个随访链路串起来了而不是每次都要手动去建计划。第五个是统计报表模块。按病种统计患者数量、按随访状态统计任务分布、按医生统计工作量、按月份统计随访完成率。报表是答辩时的加分项因为这块会用到GROUP BY、CASE WHEN、时间函数这些进阶SQL知识点有东西可讲。3.2 核心表结构设计先看患者表。字段设计上我重点关注两件事一是患者编号要有业务含义做成“前缀日期序号”的格式比如PF20240601001这样看数据一眼就知道建档时间比自增ID有意义得多二是诊断病种不要直接存字符串而是存字典表的ID方便以后按病种做统计。这个习惯在大一点的项目里是基本操作在毕设里则是亮点。CREATE TABLE patient ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, patient_no VARCHAR(32) NOT NULL COMMENT 患者编号, patient_name VARCHAR(50) NOT NULL COMMENT 姓名, gender TINYINT DEFAULT 1 COMMENT 性别1男 2女, age INT COMMENT 年龄, phone VARCHAR(20) COMMENT 联系电话, diagnosis_id BIGINT COMMENT 病种字典ID, surgery_date DATE COMMENT 手术日期, doctor_id BIGINT COMMENT 责任医生ID, status TINYINT DEFAULT 1 COMMENT 状态1在管 2转出 3失访, remark VARCHAR(500) COMMENT 备注, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_doctor (doctor_id), KEY idx_diagnosis (diagnosis_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT患者信息表;随访计划表是整个系统核心中的核心状态字段和计划时间的计算逻辑都在这张表上CREATE TABLE follow_up_plan ( id BIGINT PRIMARY KEY AUTO_INCREMENT, patient_id BIGINT NOT NULL COMMENT 患者ID, plan_type TINYINT COMMENT 计划类型1术后随访 2定期复查, plan_name VARCHAR(100) COMMENT 计划名称, cycle_days INT COMMENT 距上次节点的间隔天数, plan_date DATE NOT NULL COMMENT 计划随访日期, status TINYINT DEFAULT 0 COMMENT 0待执行 1已完成 2已逾期 3已取消, executor_id BIGINT COMMENT 执行人ID, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, finish_time DATETIME COMMENT 完成时间, KEY idx_plan_date (plan_date), KEY idx_patient (patient_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT随访计划表;这里要对plan_date和status建联合索引或者独立索引的意图说清楚系统每天都要查“今天有哪些计划待执行”和“有多少计划逾期”这两个字段是查询频率最高的过滤条件。数据量小的时候索引用处不明显但你的设计意图本身就是加分项说明你真的理解索引的使用场景而不只是会建表。随访记录表保存每次随访的执行结果。我特意把视力和眼压单独拆成字段因为这两个是眼科随访最常规的检查项目结构化存下来以后做“视力恢复状况”这类统计就很方便CREATE TABLE follow_up_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, patient_id BIGINT NOT NULL, plan_id BIGINT COMMENT 关联计划ID, record_date DATE NOT NULL COMMENT 实际随访日期, follow_method TINYINT COMMENT 随访方式1门诊 2电话 3短信, visual_acuity VARCHAR(50) COMMENT 视力情况, intraocular_pressure VARCHAR(50) COMMENT 眼压情况, subjective_desc VARCHAR(500) COMMENT 患者主诉, doctor_advice VARCHAR(500) COMMENT 医生建议, next_cycle_days INT COMMENT 下次随访间隔天数, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_patient (patient_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT随访记录表;如果你做的是全科随访而不是眼科垂直场景把检查项拆成一张“随访检查项明细表”用键值对方式存储会更通用。但眼科的话固定字段更直观查询更简单对毕设来说合适。用户表的标准写法我就不多展开了只说一个关键点password字段长度至少要60因为BCrypt加密结果固定是一个60字符的字符串。如果字段长度设成32注册用户时就会直接报Data truncation错误。3.3 状态流转设计系统里最值得画图的是随访计划的四个状态流转待执行状态下执行人点击“完成随访”就变成已完成待执行状态下计划日期过去了还没执行就变成已逾期待执行或已完成状态患者转出或者计划取消就变成已取消。“待执行转已逾期”这一步不需要人来点每天晚上定时任务扫一遍表把plan_date小于当前日期且status0的记录批量更新成2。这是定时任务在这个系统里最朴素也最有价值的用法。我第一版实现的时候把“逾期”做成了前端实时计算页面显示的时候动态判断。后来发现两个问题一是列表每次查询都要计算一遍数据量变大会拖慢响应二是统计报表里的逾期数字会跟着当天日期变化早上看的和晚上看的不一样没法复现。改成定时刷新状态字段之后数值一天一更新业务上反而更合理——医生早上上班看到的就是昨晚生成的快照跟他说“今天该随访谁”是一个意思。4. 关键实现细节登录、计划生成与报表4.1 登录鉴权和拦截器方案登录这块我直接用经典方案登录成功后把用户信息放进Session自定义一个HandlerInterceptor拦截所有未登录请求。为什么不上Spring Security或者Shiro因为这个系统属于内部管理系统角色就管理员和医生两类权限粒度只需要区分“能不能进后台”以及“部分管理功能只有管理员能用”。Spring Security配置成本高、概念多对这种体量的系统属于杀鸡用牛刀。毕业答辩问到安全方案你说“通过拦截器实现会话级访问控制”完全站得住。拦截器核心代码很简单public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User loginUser (User) session.getAttribute(loginUser); if (loginUser null) { String requestedWith request.getHeader(X-Requested-With); if (XMLHttpRequest.equals(requestedWith)) { response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\登录已过期\}); } else { response.sendRedirect(/login); } return false; } return true; } }注册拦截器时注意排除登录页面、登录接口、静态资源这些路径Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/login, /doLogin, /css/**, /js/**, /images/**, /error); } }Ajax请求和普通页面请求分开处理是我第二版才补上的。最初只做了页面跳转结果登录超时之后页面里那些Ajax请求返回的不是JSON而是登录页面的HTML前端脚本到处报解析错误。这个细节很小但实际体验影响很大建议一开始就区分开。密码存储多说一句。数据库表里密码字段用BCrypt加密每次注册或者改密码时对明文做加密再入库。MD5虽然也是哈希但可以被彩虹表快速破解必须加盐BCrypt内置随机盐每次哈希结果都不一样抗暴力破解能力强很多。Spring Security的crypto模块里就有BCryptPasswordEncoder单独引一个spring-security-crypto依赖就能用完全不需要引入整个安全框架。4.2 MyBatis的Mapper层写法与动态SQLMyBatis这块我的习惯是简单的单表CRUD用注解涉及多表联查、动态条件、分组统计的SQL写XML。注解写起来快但SQL一长就乱XML里写动态SQL则舒服很多比如患者列表这个查询要根据姓名、病种、状态多个可选条件组合筛选用where标签配合if标签就很干净select idselectPatientList resultTypecom.example.eyefollow.entity.PatientVO SELECT p.*, d.diagnosis_name, u.real_name AS doctor_name FROM patient p LEFT JOIN diagnosis_dict d ON p.diagnosis_id d.id LEFT JOIN sys_user u ON p.doctor_id u.id where if testkeyword ! null and keyword ! AND (p.patient_name LIKE CONCAT(%, #{keyword}, %) OR p.patient_no LIKE CONCAT(%, #{keyword}, %) OR p.phone LIKE CONCAT(%, #{keyword}, %)) /if if testdiagnosisId ! null AND p.diagnosis_id #{diagnosisId} /if if teststatus ! null AND p.status #{status} /if /where ORDER BY p.create_time DESC /select这个查询用LEFT JOIN把病种名和医生姓名带了出来避免在业务层做二次查询。写MyBatis时注意resultType里的VO类字段要和SQL查询列的别名对得上。我习惯把数据库实体和查询视图对象分开——实体对应表字段VO是给前端展示用的可以包含关联表冗余字段。很多同学喜欢把多表查询结果直接塞进实体类里字段空着也不管短期内能用代码一多实体类就变得臃肿后面维护想删字段又怕影响别处特别难受。分页直接用了PageHelper插件PageHelper.startPage(pageNum, pageSize); ListPatientVO list patientMapper.selectPatientList(query); PageInfoPatientVO pageInfo new PageInfo(list);必须记住PageHelper的使用禁忌startPage方法必须紧跟第一条数据库查询语句中间不要插入其他任何数据库操作否则插件会拦截错SQL导致分页莫名其妙失效。这个坑我至少帮人排了四五次每次都花不少时间定位。4.3 随访计划自动生成的定时任务自动生成随访计划是这套系统里最有“系统感”的功能也是答辩老师最喜欢提问的地方。我用SpringBoot自带的Scheduled注解实现没有引入Quartz。原因很直接系统里只有一个定时任务场景不需要分布式调度、不需要任务持久化Spring自带的能力完全够用。如果论文里写“专门引入了Quartz”反而容易被追问为什么要为一个简单的每日任务引入一套调度框架。Component public class FollowUpScheduler { Resource private FollowUpPlanService followUpPlanService; // 每天凌晨2点执行生成当天随访计划 刷新逾期状态 Scheduled(cron 0 0 2 * * ?) public void generateDailyPlans() { followUpPlanService.generateTodayPlans(LocalDate.now()); followUpPlanService.refreshOverdueStatus(LocalDate.now()); } }generateTodayPlans的核心逻辑查出所有“在管”状态的患者凡是按术后日期或上次随访日期加上间隔天数计算出的下次随访日期等于今天就生成一条待执行计划。生成前先判断当天是否已有该患者的待执行计划避免重复生成。refreshOverdueStatus就是一条UPDATEUPDATE follow_up_plan SET status 2 WHERE status 0 AND plan_date #{today}这里有个我踩过的坑定时任务方法如果做得太重比如生成计划的同时还要发短信、发站内消息一旦短信接口超时整个任务线程就会卡住后面的逾期刷新也跟着不执行。我的建议是任务方法只做数据层的生成和状态更新通知类操作丢到异步方法里用Spring的Async注解配合线程池就行。毕设系统没必要上MQ但这个拆分思路要体现出来。提醒方式我做了站内消息和短信接口两档。站内消息是一张notify表登录后后台右上角显示未读数量。短信对接的是云平台短信服务配置好accessKey和模板就能发。答辩演示时我一般不建议现场依赖短信万一现场网络不稳定接口超时就翻车了。稳妥的策略是先展示站内消息提醒再说明短信通道已经预留扩展演示前准备好测试手机号就行。4.4 统计报表里的SQL技巧报表模块用ECharts画图后端提供JSON数据接口。分享两个实用SQL写法。按病种统计患者数量SELECT d.diagnosis_name AS name, COUNT(p.id) AS value FROM patient p LEFT JOIN diagnosis_dict d ON p.diagnosis_id d.id WHERE p.status 1 GROUP BY p.diagnosis_id, d.diagnosis_name ORDER BY value DESC按月份统计随访完成率SELECT DATE_FORMAT(plan_date, %Y-%m) AS month, COUNT(*) AS total, SUM(CASE WHEN status 1 THEN 1 ELSE 0 END) AS finished FROM follow_up_plan WHERE plan_date #{startDate} GROUP BY DATE_FORMAT(plan_date, %Y-%m) ORDER BY month完成率在Java端算一下就行finished除以total再乘100。这段代码里用到了CASE WHEN做条件聚合属于SQL进阶的常规操作面试和答辩都很常问。DATE_FORMAT这个函数在不同数据库里有差异——SQL Server用FORMATOracle用TO_CHAR——如果你只熟悉MySQL换数据库马上露馅。这也是我推荐用MySQL的原因之一它的时间函数在主流数据库里算最好记的。前端ECharts的写法本身不复杂初始化一个图表实例把Ajax拿到的数据塞进series数组。唯一要注意的是如果页面是Thymeleaf渲染加ECharts图表初始化时可能因为页面还没渲染完拿不到容器元素要在window.onload或$(document).ready里执行init。这个问题在引入Vue之后会演化成另一个经典问题——DOM更新后图表不刷新需要用nextTick解决。这类踩坑就是实际开发最花时间的地方提前记住能省不少事。4.5 随访记录的Excel导出Excel导出用Apache POI实现。核心代码如下public void exportFollowUpRecords(HttpServletResponse response, Long patientId) throws IOException { ListFollowUpRecordVO list recordMapper.selectRecordsByPatient(patientId); Workbook workbook new XSSFWorkbook(); Sheet sheet workbook.createSheet(随访记录); String[] headers {随访日期, 随访方式, 视力, 眼压, 患者主诉, 医嘱, 下次随访间隔}; Row headerRow sheet.createRow(0); for (int i 0; i headers.length; i) { headerRow.createCell(i).setCellValue(headers[i]); } int rowNum 1; for (FollowUpRecordVO r : list) { Row row sheet.createRow(rowNum); row.createCell(0).setCellValue(r.getRecordDate().toString()); row.createCell(1).setCellValue(getMethodText(r.getFollowMethod())); row.createCell(2).setCellValue(r.getVisualAcuity() null ? : r.getVisualAcuity()); // 其他列按同样方式填充 } response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment;filenamerecords.xlsx); workbook.write(response.getOutputStream()); workbook.close(); }POI导出有个经常遇到的坑Content-Disposition里的文件名如果是中文浏览器下载时会出现乱码。解决方式是对中文文件名做URL编码URLEncoder.encode(随访记录.xlsx, UTF-8)再把编码后的字符串拼进header。另外如果导出数据量很大比如上万行XSSFWorkbook往内存里塞会带来明显压力这种情况要考虑SXSSFWorkbook流式导出或者直接用EasyExcel这个开源库。毕设系统一般数据量没到那个级别POI完全够用但答辩时能说出这个优化方向会很加分。5. 部署调试过程中的真实踩坑记录这一节记录的都是我自己动手时遇到的问题按发生频率从高到低排每个问题都附排查手段你遇到时可以照葫芦画瓢不用从零开始。5.1 启动阶段的典型问题第一个高频问题启动时报Invalid bound statement (not found)意思是Mapper接口和XML映射文件没有绑定上。原因一般有三个XML文件没放在application.yml配置的mapper-locations路径下XML里namespace写的不是Mapper接口的全限定名XML文件没被编译进target目录。第三个问题在IDEA里最坑——你把XML拷进resources目录如果只刷新目录没有重新构建target下面还是旧文件。排查办法是确认XML在src/main/resources/mapper/下执行mvn clean再重新启动。第二个高频问题数据库连接失败。报错信息五花八门核心就几类驱动类找不到、连接端口不对、账号密码错误、时区配置缺失。我排查时习惯先在Navicat里用完全相同的参数连一次数据库。如果Navicat能连上而Java连不上问题一定出在配置字符串或依赖上如果Navicat也连不上那就是MySQL服务本身没启动或者账号密码有问题。这个思路能帮你快速缩小排查范围不用盯着异常栈瞎猜。第三个是端口占用。8080被别的进程占了SpringBoot反复尝试启动然后报ApplicationContext failed to start。排查命令很简单Windows下执行netstat -ano | findstr 8080看到占用进程的PID后去任务管理器看是什么程序。多数情况是上次启动的Java进程没杀干净或者别的开发工具占着端口。临时改端口也方便启动命令后面加--server.port8081就能覆盖配置文件不用改文件。5.2 运行阶段的隐藏坑前端Ajax拿到的时间字段显示成一大串数字或者格式和页面预设不一致。这是Java端时间序列化的经典问题。SpringBoot默认用Jackson序列化LocalDateTime输出格式不够友好而时间戳又带时区含义前端直接展示就会错乱。可以在配置里统一处理spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai但注意对于LocalDateTime类型上面的date-format不一定生效Jackson对LocalDateTime要走jsr310模块通常需要在实体时间字段上逐个加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)。我更倾向的方式是给LocalDateTime写全局自定义序列化器但毕设里逐个加注解更直观答辩时也更好解释。另一个隐藏较深的坑MyBatis的map-underscore-to-camel-case配置。数据库字段create_time映射实体类属性createTime如果这个配置没有开启查询结果就是null。很多人查SQL没问题数据库也有数据但页面就是空白最后发现是驼峰映射没开。我前面给的application.yml里已经配了但老项目可能有各种奇怪配置注意检查。更根本的原则是数据库字段全用snake_caseJava属性全用camelCase然后开启映射整个项目统一规范就不会有这种低级问题。第三个坑是MyBatis的foreach标签批量插入数据量一大就报语法错误或者超长SQL。MySQL默认max_allowed_packet是4MB一次性插入几千条记录时SQL字符串很容易超限。处理方式就是分批插入比如每500条一批。这不算什么高级优化但能保证稳定运行。5.3 部署时的注意点打包部署上毕设系统一般就是本地部署加演示常见坑也不少。用mvn package打完包如果你用外部Tomcat还要注意SpringBoot项目的内嵌Tomcat和外部容器的兼容问题打成war包部署到外部Tomcat时需要继承SpringBootServletInitializer并重写configure方法。我觉得毕设项目没必要上外部Tomcat用内置Tomcat打成jar包直接跑最省事换台机器只要装了JDK就能跑。jar包方式运行后经常遇到“配置文件找不到”或“静态资源404”原因多半是路径写成了绝对路径或者依赖了target目录下的相对路径。SpringBoot默认从classpath:/static/目录加载静态资源文件放src/main/resources/static/下就没问题。自定义文件上传存储路径时别在代码里写死绝对路径用application.yml里配置的upload.path属性去读。数据库初始化也容易出问题。我建议把建表SQL和初始数据写成一个schema.sql用SpringBoot的spring.sql.init.modealways自动执行。但要注意这个配置每次启动都会执行SQL不是幂等的话第二次启动就会报错。稳妥做法是初始化完成后把初始化配置关掉或者SQL里统一加IF NOT EXISTS。演示前最稳的方案还是手动用Navicat导入一次数据库然后把自动初始化配置关闭一劳永逸。再补一个和MySQL版本相关的坑MySQL 8.0以上默认认证插件是caching_sha2_password如果你的JDBC驱动版本太老连接时会直接报认证插件不兼容。解决办法是把mysql-connector-java升级到8.0.x对应驱动类用com.mysql.cj.jdbc.Driver。这个错误第一次见会觉得很难解决其实就是驱动和新版MySQL的兼容问题。5.4 常见问题速查表报错现象常见原因排查/解决建议Invalid bound statement (not found)Mapper XML未编译、namespace错误、路径不对检查XML路径和namespacemvn clean重新构建数据库连接失败驱动版本、密码、时区、端口先用Navicat同参数连接验证时间字段显示乱码或时间戳Jackson序列化格式未配置配置jackson date-formatLocalDateTime加注解实体字段查询结果为null驼峰映射未开启配置map-underscore-to-camel-casetrue端口被占用进程未清理或冲突netstat -ano查PID或临时换端口中文文件名下载乱码Content-Disposition编码问题对文件名做URLEncoder.encode批量插入SQL超长MySQL的max_allowed_packet限制分批插入每批500条左右MySQL 8连接报认证插件错误JDBC驱动版本太老升级驱动到8.0.x驱动类用cj前缀6. 论文LW撰写和答辩演示的实操经验6.1 论文结构怎么安排这类选题的论文一般按软件工程标准流程写我的建议是这个顺序绪论、相关技术介绍、系统需求分析、系统设计、系统实现、系统测试、总结。这个结构最稳评审老师看着顺眼你写起来也不容易乱。相关技术介绍那一章别写太泛。不要从Java语言发展史开始凑字数重点写清楚你为什么用这些技术解决具体问题。比如MyBatis这块可以写“通过MyBatis实现数据持久化采用XML配置动态SQL以满足患者多条件检索场景”把技术选型原因和业务场景挂钩比干巴巴介绍框架语法好得多。需求分析章节一定要有用例图。管理员和医生两类角色各自的核心用例图加上患者信息管理、随访计划管理、随访记录管理、统计报表这几块。画图工具用ProcessOn或者draw.io都行画出核心用例即可不用面面俱到导师要的是这个环节存在且逻辑通顺。系统设计章节里数据库设计要逐表列出字段说明用表格呈现字段名、类型、是否主键、是否为空、字段说明。E-R图更不能少把患者、用户、随访计划、随访记录之间的关系画清楚。注意E-R图里的关系和代码里的外键逻辑要对应别图里画了一对多代码里根本没有关联查询导师较真起来很难受。6.2 演示脚本怎么准备演示环节我吃过亏说点实在的。首先演示一定用自己搭好的本地环境不要现场连远程服务器。远程数据库一旦断连整个演示直接翻车这是血泪教训。演示顺序建议按业务流走登录系统展示首页和权限差异管理员能看到系统管理菜单而医生看不到录入一个新患者给他创建随访计划展示列表和状态标识模拟今天有一条待随访计划完成随访并填写记录最后去统计页面展示图表。这套流程走完系统核心功能全覆盖全程五到八分钟节奏刚好。如果定时任务不好等我有个小技巧演示前把某条计划的计划日期手动改成今天或者干脆写一条测试计划然后在计划管理页面点“手动执行每日任务”的按钮立刻能看到状态刷新。为了方便演示我在后台加了这个手动触发按钮虽然上线版本会去掉但演示时非常好用还能顺便体现你对定时任务执行机制的理解。6.3 高概率答辩问题根据我接这类项目交流的经验把老师最可能问的问题整理一下附上回答思路。“为什么选这个课题”千万别说“题目好做”或者“导师安排的”。可以讲背景事实眼科慢性病管理需求大基层随访全靠手工信息化能提高效率和准确率。把课题意义讲清楚比什么说辞都强。“系统安全性怎么考虑”从三个层面回答登录密码BCrypt加密存储页面请求通过拦截器做会话控制数据库连接不暴露明文密码而是通过配置文件管理。这三个点是你真实实现的不要去硬扯没做过的HTTPS证书、防火墙策略之类说多了反而露怯。“数据库设计的依据是什么”用实例回答。为什么随访计划表要单独建因为一张计划可能对应多次执行状态变化而且计划状态需要被定时任务批量刷新独立成表才能高效更新。为什么患者表要和病种字典表分离因为统计报表要按病种GROUP BY。用“为什么这么做”来回答比背数据库范式有效得多。“系统还有哪些不足”这个问题几乎必问千万别答“没有不足”。准备两三个真实可讲的改进方向短信平台对接失败后的重试机制、患者档案与随访记录的数据一致性、权限系统可以扩展更细粒度的数据权限让医生只看自己负责的患者。注意控制数量说两三个就好别把系统说得一无是处。“未来的扩展方向”可以提接入在线问诊、移动端小程序、与院内信息系统对接。但要把握分寸这些只能作为展望老师的关注点是你能不能结合当前技术架构说明扩展方式。比如说到小程序就顺带一句目前后端接口已经是JSON格式移动端可以直接复用这套接口只新增一套前端界面就能落地。这样显得务实。我个人的体会是论文和演示的关键不在篇幅多长、功能多花哨而在于整个系统的逻辑能不能自圆其说。你把“患者建档、计划生成、随访执行、数据沉淀、报表反馈”这条业务链路讲透了每个技术选型都能说出依据答辩基本就稳了。最后再分享一个小细节演示前把数据库里的数据清理一下别留一堆乱七八糟的测试数据。干干净净的一位患者档案、几条随访计划、几条记录配合终端窗口里的日志输出老师看着舒服你讲起来也顺。这种小事看着不起眼但对第一印象的影响非常大。
阅读完成 · 觉得有帮助?