这个项目我上手完整跑了一遍,SpringBoot全家桶搭建的校园医保系统,从数据库初始化到前后端联调踩了不少坑,也有不少值得展开说的设计细节。如果你正在找JavaWeb方向的课设选题,或者想拆一套完整的权限管理+业务流转案例,这篇复盘值得花几分钟看完。
1. 系统整体设计与需求拆解
1.1 校园医保业务的真实场景
校园医疗保险和社保医保不太一样,它更像一个校内福利性质的补充保险。学生每学年缴纳几十到上百不等的保费,在校医院或者指定医疗机构看病,然后凭票据走校内报销流程。传统做法是纸质申请表+人工审核,医保办老师对着Excel表核对,到了报销高峰期,办公桌上堆的全是病历本和发票。
这套系统要解决的,就是把“学生提交申请、校医保办审核、财务打款记录、参保信息维护”这条链路搬到线上。标题里写的是“校园医疗保险管理系统18525”,18525应该是数据库表设计时的一个编号或者作者的项目流水号,不用太纠结。核心在于它把业务拆成了参保管理、报销申请、审核流转、政策发布、统计查询五大板块。
如果你是刚开始做课设或者想拿这套系统练手,最值得学的不是CRUD本身,而是它怎么把角色权限和业务流程绑定在一起。学生只能看到自己的报销记录,医保办老师能审所有人的单据,系统管理员管用户和基础配置,这种数据隔离逻辑在真实项目里非常常见。
1.2 六类核心功能模块拆解
系统完整的功能结构大致如下:
| 模块 | 功能点 | 目标用户 |
|---|---|---|
| 个人中心 | 修改密码、查看个人信息 | 全部角色 |
| 参保管理 | 参保信息登记、续保、停保 | 学生、医保办 |
| 报销申请 | 提交报销单据、上传凭证 | 学生 |
| 审核管理 | 材料审核、报销比例核算 | 医保办 |
| 政策公告 | 医保政策发布与查看 | 医保办、学生 |
| 系统管理 | 用户管理、角色分配 | 管理员 |
这个模块划分几乎可以照搬去套大多数管理类系统,区别只在业务字段不同。比如报销申请表里有就诊日期、医疗机构名称、总费用、医保内费用、报销比例、实际报销金额、发票编号、审核状态、审核意见这些字段,换成一个设备报修系统,字段就变成了设备编号、故障描述、报修人、处理人。
界面是典型的Bootstrap风格后台管理页面,左侧菜单栏+右侧内容区,表格上方有搜索框,数据列表支持分页。这套界面模板的优点是开发速度快,不用自己写复杂的前端交互,后端人员一天就能把页面全部铺完。
2. 核心技术选型与关键配置
2.1 技术栈选型背后的取舍
后端用的SpringBoot,持久层配的MyBatis,数据库是MySQL,前端以Bootstrap和Thymeleaf为主。这套组合在JavaWeb课设和毕业设计里几乎是统治级的存在,原因是逻辑清晰、资料多、调试方便。
SpringBoot简化了Spring的Bean配置过程,内嵌Tomcat让打包部署变成一个可执行Jar,不用再单独装Servlet容器。MyBatis把SQL写在XML里,复杂查询好调整,不用像JPA那样让框架猜SQL语义。MySQL的InnoDB引擎支持事务和外键,报销这种需要多表关联更新的场景必须有事务保护。
为什么不选微服务、不选前后端完全分离?因为这套系统的目标场景是单机部署、几十个并发、内部使用,SpringCloud那套反而把简单问题搞复杂了。前后端分离需要单独处理跨域、Token刷新、接口文档维护,如果前端功底一般,开发周期至少多出三天。SpringBoot集成Thymeleaf让后端直接把数据渲染到HTML里,学生登录后打开报销页面能看到自己的申请列表,整个流程走起来非常顺。
2.2 application.yml核心配置解读
整份配置里最关键的三个文件是pom.xml、application.yml和数据库初始化脚本。pom里引入了SpringBoot Web、MyBatis、MySQL驱动、Druid连接池和Lombok依赖,核心片段是这样的:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.20</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>MyBatis版本这里我用的是2.3.1,配合SpringBoot 2.7.18完全没问题。如果强行升级到MyBatis 3.5.16+SpringBoot 3.x,需要把javax包全部替换成jakarta,很多教程里的代码直接跑不起来。
application.yml里重点看数据源配置:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/medical_insurance?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 type: com.alibaba.druid.pool.DruidDataSource mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.insurance.entity configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case这个配置建议打开,数据库字段insure_date就能自动映射到实体类的insureDate属性,省去大量resultMap手写工作量。serverTimezone这个参数也值得注意,不配的话,高版本MySQL驱动会报时间区域错误。
Redis在这里没有引入,因为学生医保系统的会话状态不复杂。登录成功后把用户对象存进Session,用拦截器校验是否已登录,角色通过URL权限控制,spring-security不是必须引入的。只有权限粒度细到每个按钮时,才需要引入Shiro或Security做注解鉴权,这个系统用拦截器+Session方案完全够用。
2.3 数据库表结构设计思路
系统核心表我梳理了一下,一共8张左右:用户表、角色表、参保信息表、报销申请表、报销明细表、政策公告表、机构信息表、操作日志表。
报销申请表是核心中的核心,它把一次报销申请的所有必要信息都记录下来:
CREATE TABLE `reimburse_apply` ( `id` int(11) NOT NULL AUTO_INCREMENT, `apply_no` varchar(32) DEFAULT NULL COMMENT '申请单号', `student_id` int(11) DEFAULT NULL COMMENT '学生用户id', `hospital_name` varchar(100) DEFAULT NULL COMMENT '就诊机构', `visit_date` date DEFAULT NULL COMMENT '就诊日期', `total_amount` decimal(10,2) DEFAULT NULL COMMENT '总费用', `insured_amount` decimal(10,2) DEFAULT NULL COMMENT '医保内费用', `reimburse_ratio` decimal(5,2) DEFAULT NULL COMMENT '报销比例', `reimburse_amount` decimal(10,2) DEFAULT NULL COMMENT '报销金额', `bank_card` varchar(30) DEFAULT NULL COMMENT '收款银行卡号', `status` tinyint(1) DEFAULT '0' COMMENT '0待审核 1通过 2驳回', `audit_opinion` varchar(255) DEFAULT NULL COMMENT '审核意见', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;报销比例为什么不直接写死在代码里,而是以字段形式存在?因为校医院、校外定点医院、异地就医这三类场景报销比例不同,如果写死在代码里,每次调整政策都得改代码重新打包。存进数据库之后,医保办老师自己在页面上改就行。这也是设计上比较聪明的一点。
学生表通过student_id关联报销申请表,查询学生报销记录时用userId去用户表反查学号、姓名、学院信息,避免在报销表里冗余太多字段。这种设计在数据库第三范式上站得住脚,但查询时多一次表关联。如果数据量超过十万条,可以适当冗余,比如在报销表里直接存学生姓名和学院,这是典型的空间换时间优化。
3. 核心模块实现与业务逻辑走通
3.1 登录鉴权与角色控制实现
登录接口的设计非常直接,前端提交用户名和密码,后端接收后用Md5加密和数据库比对,成功后跳转到对应用户首页。
@PostMapping("/login") public String login(String username, String password, HttpSession session, Model model) { // 密码加密后查询 String md5Password = DigestUtils.md5DigestAsHex(password.getBytes()); User user = userService.login(username, md5Password); if (user != null) { session.setAttribute("loginUser", user); return "redirect:/index"; } model.addAttribute("error", "用户名或密码错误"); return "login"; }密码加密这里用的Spring自带的DigestUtils,简单够用。正式项目推荐加盐,或者直接用BCrypt,否则遇到彩虹表碰撞,弱口令很容易被反解出来。学生端密码默认一般是学号后六位,医保办老师的初始密码是系统管理员设定的,首次登录后强制要求改密。这个强制改密码的功能在拦截器里判断,如果标记是首次登录就跳转到修改密码页面。
拦截器实现登录校验的核心代码不建议写在Controller里,用Spring的HandlerInterceptor统一处理更优雅:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); User user = (User) session.getAttribute("loginUser"); if (user == null) { response.sendRedirect("/login"); return false; } return true; } }3.2 报销申请模块的完整走通
学生登录后提交报销申请,页面上的关键字段是总费用、医保内费用和就诊日期。后端拿到数据后要校验医保内费用是否大于总费用、报销金额是否超过上限,这些校验逻辑放在Service层,前端只做基础非空校验。
核心流程是这样的:
- 学生填写报销申请,上传门诊发票照片或PDF
- 生成申请单号(格式:BX+年月日+随机三位数,如BX20241220001)
- 医保办老师查看待审核列表,点击详情核对票据
- 老师录入审核意见,选择通过或驳回
- 通过后系统计算报销金额,进入待打款状态
- 财务人员确认打款,状态变为已完成
这里的生成申请单号的方法值得学习:
public String generateApplyNo() { String dateStr = new SimpleDateFormat("yyyyMMdd").format(new Date()); String randomNum = String.format("%03d", new Random().nextInt(1000)); return "BX" + dateStr + randomNum; }这个方法在并发高的情况下可能会生成重复号,比如同一秒内两个学生同时提交。实战里可以把随机数换成数据库自增id,或者用Redis生成序号。课程设计层面这个写法没问题,但要是在真实生产环境,建议改成雪花算法或者数据库序列。
3.3 分页查询与条件检索的经典写法
报销列表页支持按状态筛选、按学号搜索、按日期范围查询,这里涉及的就是典型的MyBatis动态SQL+分页插件。
public PageResult<ReimburseApply> queryApplyList(int page, int limit, ReimburseApplyQuery query) { PageHelper.startPage(page, limit); List<ReimburseApply> list = reimburseApplyMapper.selectByCondition(query); PageInfo<ReimburseApply> pageInfo = new PageInfo<>(list); return new PageResult<>(pageInfo.getTotal(), pageInfo.getList()); }PageHelper用起来确实方便,但有个坑得提醒一下:startPage方法必须紧跟在要分页的查询语句之前,中间如果插入其他数据库操作,PageHelper会把分页参数绑定到错误的SQL上。我遇到过一种情况,Service层先是查询了一条系统配置,然后才执行列表查询,结果分页完全不生效,查源码才发现是这个原因。最稳妥的方式是让分页查询直接写在Mapper调用的上一行。
3.4 前端界面与接口联调的细节
前端的页面文件在src/main/resources/templates目录下,layouts.html是公共布局模板,左侧菜单在这里统一配置。菜单项根据登录用户角色动态渲染,用Thymeleaf的sec:authorize或自定义的条件判断实现。
页面和Controller之间通过Model传值。Controller里return "reimburse/list"对应找templates/reimburse/list.html,Thymeleaf的th:each可以循环渲染表格行,th:if控制审核按钮在不同状态下显示还是隐藏。如果学生提交的申请正在待审核状态,页面上显示“撤销申请”按钮,一旦审核通过按钮消失。
Bootstrap的大栅格系统在电脑端浏览器的呈现效果很好,但如果在手机上打开,表格会有一点点拥挤。这套系统本来就是面向PC后台操作的,不建议花太多精力在移动端适配,能用就行。
前端和后端联调时最常踩的坑是浏览器缓存。修改了JS文件但界面没变化,按Ctrl+F5强制刷新一般能解决。还有static目录下新加了一个CSS文件,Thymeleaf引入时手误把路径写错了,页面样式直接崩溃,检查network面板就能定位到404。
4. 从零跑通项目的完整实战记录
4.1 开发环境准备与工具清单
这套项目的标准环境组合如下:
| 工具 | 版本建议 |
|---|---|
| JDK | 1.8或11 |
| Maven | 3.6.3以上 |
| MySQL | 5.7或8.0 |
| IDE | IDEA 2020及以上 |
| 数据库客户端 | Navicat或DataGrip |
JDK版本是第一个坑。很多人电脑上装了JDK17甚至JDK21,但SpringBoot 2.7基于javax命名空间,在JDK17下编译没有问题,但有些老版本插件不兼容。如果用的是JDK17,建议把SpringBoot版本提到2.7.18,这样兼容性最好。
Maven仓库下载依赖慢的问题,直接修改settings.xml里的阿里云镜像,几分钟就能把依赖全拉完。
4.2 数据库初始化与项目启动过程
拿到源码后,数据库初始化分三步:
- 新建数据库medical_insurance,字符集选utf8mb4、排序规则选utf8mb4_general_ci
- 将sql文件导入,我一般直接用Navicat的“运行SQL文件”功能,选择数据库后执行即可
- 修改application.yml里的数据库名、用户名、密码
导入SQL时如果报错“Unknown database”,检查是不是忘了先创建数据库。如果报错语法错误,大概率是SQL文件版本和MySQL版本不匹配,MySQL 8.0下导入某些老SQL文件时排序规则可能会报错,在文件里把utf8mb4_general_ci替换成utf8mb4_0900_ai_ci就行。
启动项目,先在IDEA里点击mvn spring-boot:run,或者在命令行执行:
mvn clean package -DskipTests java -jar target/medical-insurance-0.0.1-SNAPSHOT.jar看到“Started Application in 3.2 seconds”的日志就代表启动成功了,浏览器访问http://localhost:8080,页面能正常加载就说明整个过程是通的。数据库连接失败一般会报红色错误日志,检查MySQL服务有没有启动,以及账号密码是否正确。
Druid连接池还提供了监控页面,项目启动后访问/druid/login.html,输入配置的用户名密码能看到SQL执行耗时、连接池活跃数等数据。这个功能排查慢查询很有用,报销列表页如果响应太久,就可以去看哪条SQL消耗了最多时间。
4.3 数据库同步工具在项目调试中的实战用法
这里我要特别聊一个很多人没注意的细节。开发环境数据库数据更新频繁,本地库和服务器库经常不一致,反复手动导入导出SQL文件很费事。我在跑这套项目时用了数据库同步工具搭配自动化逻辑,极大减少了重复劳动。
工具选择上比较推荐Navicat的结构同步功能,它可以比对两个连接的差异,然后生成更新脚本。比如本地库新增了一张表,服务器库还没有,Navicat会对比出缺失的表结构,一键生成ALTER语句,手动执行或者让工具直接同步都能完成。
另一类工具是类似Flynx的数据库同步软件,配置完源数据库和目标数据库连接信息后,选择需要同步的表,可以设置定时任务每隔几分钟自动拉取增量数据。这套项目是单机部署,不涉及复杂的生产环境多库同步,用它做开发库和服务器库之间的单向同步就够了。
如果你的团队里有多个人同时在改一个项目,还是建议把SQL变更统一提交到Git仓库,每次修改执行一个增量SQL文件,这样版本变更可控可回滚。数据库同步工具适合自己一个人开发、两边库结构不一致又不想手工整理的情况,操作前一定要先备份目标库,防止误同步覆盖数据。
4.4 前后端登录调试的关键步骤
项目跑起来后,第一个要验证的就是登录功能。学生角色的初始账号在SQL文件里已经插好了,用户名一般是学号,密码是学号后六位。医保办老师的账号通常是admin开头,初始密码123456,具体以数据库里user表的数据为准。
登录不了的时候,按这个顺序排查:
- 先看控制台日志,如果是Unknown column报错,说明user表结构和实体类不对应
- 如果提示用户名或密码错误,到数据库里把user表里对应记录的password字段打印出来,手动用MD5工具生成一遍密码比对一下
- 如果跳转到了500页面,大概率是Session取值的类名导错了或者Thymeleaf模板有语法错误
我在第一次跑这套项目的时候,密码一直验证不过,后来发现是安装MySQL时选择的大小写敏感配置导致的,表名是大写还是小写在Linux上会有影响,Windows上反而没事。改一下MySQL配置lower_case_table_names=1就好,改完记得重启MySQL服务。
5. 高频报错与排查技巧实录
5.1 常见错误速查表
我把跑项目过程中的报错整理成表,方便后来者对照排查:
| 错误现象 | 根本原因 | 解决方案 |
|---|---|---|
| 端口被占用 | 8080被其他服务占用 | 把application.yml里server.port改成8081 |
| java.sql.SQLNonTransientConnectionException | 数据库连接不上 | 检查MySQL服务、用户名密码、数据库名 |
| 登录后页面404 | 静态资源路径错误 | 确认/source目录下模板文件位置和Controller返回值路径一致 |
| Invalid bound statement | Mapper接口方法和XML里的id不一致 | 核对namespace和方法名 |
| Cause: java.lang.IllegalArgumentException | 参数个数不匹配 | 检查@Param注解是否添加 |
| Whitelabel Error Page | Controller没有捕获异常 | 添加@ControllerAdvice全局异常处理器 |
| Druid监控页面访问不了 | 没有放行拦截器 | 在拦截器配置里exclude /druid/** |
| 上传文件超过大小限制 | SpringBoot默认单文件1MB | 配置spring.servlet.multipart.max-file-size=10MB |
表格里的前三条出现频率最高,其中数据库连接失败占到了一大半。排查数据库连接问题的思路是先在Navicat里测试一下能否连上,如果能连上但程序报错,再去检查配置文件。如果Navicat也连不上,那就得看MySQL服务本身有没有启动。
端口冲突也很常见,尤其装了某些应用会自动占用8080。用netstat -ano | findstr 8080查到是哪个进程占用了端口,然后结束进程或者改项目端口都行。
5.2 两个容易忽略的坑
第一个坑是Druid和MyBatis的兼容问题。MyBatis-Plus版本如果太低,和Druid连接池一起使用时会报ClassNotFound,原因是没有引入mybatis-spring-boot-starter的对应版本。解决办法是把MyBatis starter版本和SpringBoot版本对齐,或者在pom里显式引入mybatis-spring依赖。
第二个坑是数据库字符集导致的中文乱码。虽然application.yml里写了characterEncoding=utf8,但是MySQL数据库本身如果建成了latin1,查询出来依然是乱码。导入SQL之前先确认数据库的字符集,执行show variables like 'character%'查看结果,如果不是utf8mb4,需要先ALTER database修改字符集。
这两个坑都不会报特别明显的错,乱码容易看出来,兼容问题一般只在启动时出现一次异常。比较隐蔽的是MyBatis打印出的SQL里中文条件正常,但数据库里查询不到结果,这种基本就是字符集不一致导致索引无法正常匹配。
5.3 部署到服务器时的实战技巧
本地跑通只是第一步,部署到远程服务器时有一些差异点需要注意。打包时记着执行mvn clean package -DskipTests,不要跳过测试类的时候把编译也给跳了。
数据库同步工具在服务器部署这里又能派上用场。如果本地和服务器数据库版本不一样,直接用SQL文件导入可能会出问题。MySQL 5.7的SQL文件导到MySQL 8.0里时,有些列属性定义会有细微差异,这种时候用数据库同步工具做结构比对再同步,会更安全一些。
部署运行时使用nohup命令让项目在后台保持运行:
nohup java -jar -Xms256m -Xmx512m /opt/medical/medical-insurance-0.0.1-SNAPSHOT.jar > /opt/medical/log.log 2>&1 &-Xms和-Xmx分别设置JVM的初始内存和最大内存。服务器内存有限的情况下,把最大内存压到512m也能跑,但并发高的时候可能会出现频繁GC。如果是2核4G的服务器,建议-Xmx开到1g。启动完成后,用tail -f /opt/medical/log.log看启动日志,一直到看到“Started Application”输出了才算启动成功。
再提醒一句:服务器上的防火墙和安全组要放行项目对应的端口,否则浏览器访问不了。云服务器一般有两层防火墙,一层是云平台的安全组,一层是Linux系统自带的firewalld,两层都得放行。
6. 这套项目的后续扩展方向
6.1 业务功能增强
跑通这套系统后,如果你想继续加深理解或者让项目在答辩时更有亮点,有几个方向值得试试。
一个是增加统计报表模块。医保办老师每个月需要知道“有多少学生提交了报销、总报销金额是多少、哪个学院的报销频次最高”,这些数据如果靠人工从Excel里统计非常累。在后端用SQL的GROUP BY加上时间范围筛选,到Controller层封装成接口,前端引入ECharts画柱状图和饼图,功能做完之后整个系统的实用性会提升很多。
另一个方向是接入短信通知或者邮件通知。报销申请审核通过或驳回后,学生希望第一时间收到结果。在审核Service的方法里,状态变更后调用阿里云短信接口或者Spring的JavaMailSender发送邮件,这个功能不需要复杂的架构改动,费用也不高,很多本科生毕设都会选这个作为创新点。
6.2 性能与代码结构优化
从代码维护角度看,Controller层的逻辑偏厚,可以引入Service接口和实现类分离的规范做法。目前是统一定义Service类,如果后续有好几个人一起开发,接口隔离会避免冲突。
数据访问层可以引入通用Mapper或者升级到MyBatis-Plus,这样单表CRUD不用手写XML,直接用BaseMapper提供的方法,开发效率提升明显。要注意MyBatis-Plus的分页插件用法和PageHelper不太一样,它需要配置MybatisPlusInterceptor,设置PaginationInnerInterceptor。
最后,项目可以考虑引入日志框架统一管理操作记录。目前是基于Logback默认配置的,可以在logback-spring.xml里按天切割日志文件,保留30天内的日志。这个配置不仅方便问题排查,在项目答辩时也可以作为“便于运维”的设计亮点。
6.3 个人实践总结
卡了最久的问题反而是最简单的数据库连接失败,查了一个多小时才发现是密码中包含了@符号,而YAML文件里没有用引号包裹,导致配置解析异常。这类细节很琐碎,但恰恰是实际开发中每天都会踩的坑。
建议你拿到源码后不要急着写代码,先花半小时读一下Controller层和Mapper层的对应关系,弄清每个URL是怎么映射到方法、再转发到哪个页面的。把这个流程摸透了,后面无论改功能还是加页面,思路都会清晰很多。我自己就是从这套项目完全跑通开始,逐步把SpringBoot相关知识点梳理成一个完整链路,再回头看其他系统,很多模块设计思路都是相通的。