拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于SpringBoot的医疗报销系统:设计、实现与部署实战

基于SpringBoot的医疗报销系统:设计、实现与部署实战

1. 项目概述与业务逻辑拆解

接触过不少毕业设计选题和中小型企业内部系统搭建的案例,医疗报销系统几乎是最能锻炼完整业务闭环能力的项目之一。表面上看,它就是一个“提交报销单、领导审批、财务打款”的简单流程,但真等到你动手设计表和接口的时候,会发现里面埋着不少坑:多级审核状态怎么流转、报销金额怎么按规则计算、单据里那些五花八门的明细怎么归类、不同角色看到的操作按钮如何区分,这些都不是CRUD能直接糊弄过去的。而这个“基于SpringBoot的医疗报销系统”恰恰把所有关键问题都串在了一起,同时技术栈主流、成本低、演示方便,非常适合做毕设项目或内部管理工具的练手原型。

我把它拆开看,核心解决的就是三件事:让患者或员工能快速提交医疗费用单据,让审核人员能规范、可追溯地完成审批,让财务和管理层能实时看到资金使用与报销的统计数据。这里面的“报销”不只是填个数字,它牵扯到费用明细、政策规则、审核留痕、状态变更、消息提醒等一连串动作,本质上是一个轻量级的BPM(业务流程管理)系统,只不过业务流程长在了医疗报销这个具体场景上。

这个系统适合谁来参考?一类是正在挑毕设题目的计算机专业学生,另一类是想快速给公司或社区做一个报销管理工具的后端开发。前者需要的是完整、能答辩、能演示的功能闭环,后者需要的是流程清晰、能扩展、能上生产的工程结构,而这套基于Spring Boot实现的项目方案刚好能同时覆盖两边的诉求。

我为什么要说“基于Spring Boot”是这个项目成立的前提?因为Spring Boot把SSM(Spring + Spring MVC + MyBatis)时代最折磨人的Bean装配、事务配置、数据源管理全部变成了自动化处理,写业务逻辑时你几乎不会为“配置”分心,这对医疗报销这种重业务逻辑的系统来说等于多了一整层开发效率。而且Spring Boot启动快、独立部署方便,打包成一个Jar就能跑,无论你是拿它写答辩演示,还是把它打成镜像丢到服务器上跑,都极其顺手。

2. 技术选型与整体架构

2.1 后端主打Spring Boot 2.x,结合MyBatis-Plus提升开发效率

选择Spring Boot版本时,不要一味追新。很多人在项目启动第一天就用了Spring Boot 3.x,结果发现JDK版本要求17+、部分老依赖不兼容、网上能找到的绝大多数参考资料都还是2.x的写法,折腾半天还没开始写业务代码。我的建议是:如果目标是快速完成一个正式可用的系统,Spring Boot 2.5.x或2.7.x加JDK 8是最稳妥的组合,生态最成熟,踩坑成本最低。

ORM框架方面,MyBatis会配合项目出现,但我个人更推荐MyBatis-Plus,它对单表CRUD的增强太友好了。写报销单明细、用户信息这些基础增删改查时,直接用BaseMapper里封装好的方法就够了,不需要手写一大串XML。更重要的是,MyBatis-Plus的分页插件配置很简洁,对报销单列表这种必然出现的分页查询场景来说,几乎是零成本接入。当然,如果你的实训要求里明确写了要手写SQL,那保留MyBatis的XML方式也完全可以,两种方案在这个项目里可以平滑切换。

另外,这个项目里我强烈建议引入一个轻量级的认证方案,比如JWT加拦截器。医疗报销系统天然涉及多角色权限,员工、科室主任、财务审核员、系统管理员各有各的操作边界。Spring Boot里实现JWT登录校验并不复杂,拦截器加一个HandlerInterceptor,对需要鉴权的接口做统一校验,就能把“谁在操作、能否操作”这个问题管理得很清楚。

2.2 前端方案:Vue或经典模板引擎按需选择

前端是这类项目最容易被低估的部分。很多同学写完后端接口,随便套一个简单页面就交差了,答辩的时候被老师点出“界面粗糙”,其实很亏。因为医疗报销系统的核心价值就体现在“填单、审批、统计”这三个操作页面上,页面交互顺不顺直接影响使用体验。

预算和时间充足,就用Vue 3加Element Plus做前后端分离。员工端报销单填写页需要弹窗选药品明细、上传发票图片;审核端需要列表筛选、审批操作按钮;统计端需要折线图、柱状图展示报销趋势。Vue生态里的组件库和ECharts都能很好地覆盖这些需求,做出来效果比较专业。

如果不想搞得太复杂,直接在Spring Boot的resources/static目录下写一套原生的HTML加Vue CDN,或者干脆用Thymeleaf模板引擎渲染页面也可以。前端文件做好后打包放进Spring Boot中,启动一个Java进程就能同时提供页面和接口,让系统整体演示部署成本降到最低。这也是我在热词里看到“vue打包放进springboot中”这个话题高频出现的原因,这套做法确实非常实用。

2.3 数据库与文件存储:MySQL打底,对象存储解决附件上传

数据存储方面,MySQL是这个项目当仁不让的首选。报销系统的数据模型核心是用户表、报销单表、报销明细表、审核记录表,它们之间有明确的外键关联和状态流转关系,用MySQL这种成熟的关系型数据库来管理最合适。要注意的是,数据库设计时要提前考虑多公司或多院区的场景,核心业务表最好都带上org_id、dept_id这类租户标识字段,方便系统将来扩展。

文件存储更需要提前想清楚。报销单里“上传票据图片”这个功能听起来简单,但如果直接往数据库里塞Base64图片,数据库会迅速膨胀,查询性能也会被拖垮。稳妥的做法是在本地服务器上提供一个上传目录,通过Spring Boot的MultipartFile接口保存文件,再把文件路径存到数据库里。如果对技术先进性有追求,热词里也有人提到MinIO这种开源对象存储,把MinIO加到Spring Boot项目中作为附件存储组件,好处是文件与业务服务解耦,备份和迁移都很方便。毕设项目用MinIO确实能成为答辩的加分项,但要先评估部署成本,别为了炫技把核心业务耽搁了。

3. 核心模块设计与数据库建模

3.1 业务模块划分:从角色权限到统计报表

医疗报销系统从功能面看,大致可以拆成五大模块:用户认证与权限管理、报销单管理、审核流程管理、系统配置管理、统计报表管理。

用户认证与权限管理解决的是“谁能登录、谁能操作什么”的问题。建议引入RBAC模型,用户和角色关联,角色和菜单权限关联。员工登录后只能看到“我要报销”和“我的报销记录”,审核员登录后看到“待审核列表”,管理员则维护角色菜单分配。Spring Boot中做RBAC并不复杂,用三张基础表(用户表、角色表、权限表)加两张关联表就能表达清楚。

报销单管理是系统的心脏。员工填写报销申请时,需要选择报销类型(门诊、住院、药品、检查等),录入总金额,逐条添加费用明细,上传对应的发票或证明文件。这里有一个细节容易被忽略:报销单的状态不应该只有“已提交”和“已通过”,而是要拆分成待审核、审核中、已通过、已驳回、已打款、已撤销等多个状态,每一步操作都要留痕。

审核流程管理解决的是“流程怎么走”。一个完整的报销审核流通常包括科室初审、财务复审、领导终审三个节点。前后两个节点之间是串行关系,前一个环节不通过,后一个环节就不会被激活。实现时,我建议不要写死状态判断,而是在数据库里增加审核记录表,用一条条审核记录推进状态机流转。

系统配置管理解决的是“规则怎么配”。比如报销比例、报销限额、可报销药品目录、审核人设置,这些都应该是可配置的,而不是硬编码在Java代码里。用数据字典表来管理这些参数,后续调整业务规则时只需要改数据,不需要改代码。

统计报表管理则是给管理层看的驾驶舱。可以统计月度报销总金额、各科室报销排名、各报销类型占比、待审核单据数量等。用ECharts这类前端图表库展示,数据的价值一下子就能体现出来。

3.2 报销流程状态机设计:用一张审核记录表支撑完整流程

做报销类系统,最忌讳的就是只用一行“status”字段存状态,审核操作时直接改掉状态就完事。这种做法的隐患在于:一旦需要追溯“这个单子到底是谁在什么时候驳回的、驳回原因是什么”,系统毫无依据。

正确做法是设计一张审核记录表,把每次操作当作一条独立记录插入进去。一张报销单在生命周期内可能经历提交、科室初审通过、财务复审通过、领导终审驳回等多次操作,每次操作产生一条audit_log,记录操作人、操作动作、操作时间、审批意见和操作后的状态快照。报销单主表上的status字段反而可以退化为一个“当前状态索引”,用于列表筛选和快速展示,真正的过程数据全部沉淀在审核记录表里。

状态流转的控制也需要专门处理。我建议不要在Service层里散落着一堆if-else判断,而是抽出一个状态机组件,统一维护“从什么状态可以转向什么状态”。比如“待审核”只能转向“审核中”和“已驳回”,“审核中”只能转向“已通过”和“已驳回”,“已通过”只能转向“已打款”。这样即使后续增加审核节点,改动也能集中在一个类里,而不是到处打补丁。

3.3 核心数据表结构设计要点解析

数据库表设计是这个项目最看重功底的环节。下面列出几张核心表的字段设计思路,不一定要求照着抄,但这些字段背后的业务含义值得参考。

用户表除了常规的id、username、password外,建议加上real_name、id_card_no、phone、org_id、dept_id这些业务相关字段。医疗报销场景里,报销单需要关联到具体员工,而员工往往属于某个科室或部门,后续统计科室报销数据时就能直接关联。

报销单主表字段要重点关注:reimb_no(报销单号,唯一索引,便于检索和展示)、user_id(申请人)、type(报销类型,用字典编码)、total_amount(总金额)、status(当前状态)、apply_time、remark。total_amount这个字段我建议在提交时由后端重新计算,不能直接信任前端传来的数字,细节见后文“费用计算”。

报销明细表要记录每条费用明细的项目名称、项目类型、单价、数量、金额、是否纳入报销范围。有些系统还会把医保目录匹配做到这一步,判断“这个药是否属于报销目录内”,这是一个很好的加分点。

审核记录表的字段包括:reimb_id、auditor_id、action(提交/通过/驳回/打款/撤销)、comment(审批意见)、created_time。我再额外推荐加一个node字段表示当前审核节点,因为多级审核下,同一个“通过”动作发生在不同节点意义完全不同。

4. 实操构建与核心代码实现

4.1 从零搭建Spring Boot项目骨架

创建一个Spring Boot项目的路径非常多,最省事的方式是直接用IntelliJ IDEA的Spring Initializr新建项目,选择Java 8、Spring Boot 2.7.x。依赖方面,基础必选的有Spring Web、MyBatis-Plus框架(手动引入)、MySQL驱动、Lombok,再按需加上Spring Validation做参数校验,加一个JWT工具库jjwt处理认证。引入依赖之后,我习惯先把Maven仓库镜像切换到阿里云,否则第一次构建项目下载依赖会很痛苦。

项目包结构建议按业务模块划分,而不是按技术层次划分。比如:

com.example.medical ├── common # 通用返回结果、异常处理、工具类 ├── config # 配置类 ├── controller # 接口层 ├── service # 业务层 ├── mapper # 数据访问层 ├── entity # 实体类 ├── dto # 接收参数的对象 └── vo # 返回给前端的结果对象

这种结构的优势是每个人都能快速定位代码位置,而且后续写毕业设计论文时,模块划分可以直接映射到系统设计章节里。不过多解释为什么,用起来就知道舒服了。

配置文件中重点设置数据源和MyBatis相关配置。MySQL 8以上记得加上时区参数:

spring: datasource: url: jdbc:mysql://localhost:3306/medical_reimb?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

4.2 登录认证与权限拦截:JWT + 拦截器的经典组合

医疗报销系统的所有业务接口都不能裸奔,必须先解决登录认证。我采用的方案是:用户登录成功后,后端生成一个JWT令牌返回给前端,前端在后续请求的Header中携带该令牌,后端通过拦截器统一校验。

先生成一个JWT工具类,核心方法包括生成Token和解析Token:

public class JwtUtil { private static final String SECRET = "your-secret-key"; private static final long EXPIRE_TIME = 1000 * 60 * 60 * 24; // 24小时 public static String generateToken(Long userId, String username, String role) { return Jwts.builder() .setSubject(username) .claim("userId", userId) .claim("role", role) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }

注意这里签名密钥建议放到配置文件中,用Spel表达式注入,不要写死在代码里。密钥也不要用我示例里这种简单字符串,正式项目里至少得是32位以上的随机字符串。

登录接口的实现也有一点讲究。查询用户时要把数据库里存的密码哈希值取出来比对,不要用明文密码。Spring Security里自带的BCryptPasswordEncoder可以很方便地做密码哈希,即使不引入完整的Spring Security,单独把这个工具类拿过来用也完全可以。

写登录接口:

@PostMapping("/login") public Result login(@RequestBody @Valid LoginDTO dto) { User user = userService.getByUsername(dto.getUsername()); if (user == null || !passwordEncoder.matches(dto.getPassword(), user.getPassword())) { return Result.error("用户名或密码错误"); } String token = JwtUtil.generateToken(user.getId(), user.getUsername(), user.getRole()); return Result.success(new LoginVO(token, user)); }

用户登录成功之后,要写一个全局拦截器校验Token。实现HandlerInterceptor接口,在preHandle方法里从请求头中获取Token并解析,如果解析失败直接返回401。注意放行登录接口本身,否则会出现“无法登录”的死循环。

4.3 报销单提交与审核流程实现:状态机与事务控制

报销单提交接口是整个系统里业务含量最高的接口。它接收一组报销单DTO数据,包括报销类型、总金额和明细列表,后端要做的事情远远不止insert一张表。

第一,金额校验。前端传来的total_amount并不可信,后端必须根据明细列表重新计算一遍总金额。如果明细里包含了“是否纳入报销”的标记,需要只累加可报销的部分。这样做的好处是一方面防止用户篡改数据,另一方面答辩时老师问起业务逻辑,你有理有据。

第二,插入操作要在同一个事务里完成。报销单主表、报销明细分表、审核记录表(提交动作也算一条审核记录)必须同生共死,任何一步失败都要回滚。使用@Transactional注解是个好习惯,但要留意本文第5部分要讲的自调用失效问题。

第三,生成报销单号。格式建议“RB”加年月日加序列号,比如RB202501150001。这个单号可以作为业务主键展示给用户,而数据库自增id则用作内部关联。生成逻辑最简单的做法是查询当天已有的单数,加一后拼接,注意加唯一索引防并发重复。

提交报销单的核心代码大致长这样:

@Transactional(rollbackFor = Exception.class) public ReimbursementVO submitReimbursement(ReimbSubmitDTO dto, Long userId) { // 1. 后端重新计算总金额 BigDecimal total = dto.getItems().stream() .filter(ReimbItemDTO::getIncluded) .map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add); // 2. 插入报销单主表 Reimbursement reimb = new Reimbursement(); reimb.setReimbNo(generateNo()); reimb.setUserId(userId); reimb.setType(dto.getType()); reimb.setTotalAmount(total); reimb.setStatus("PENDING"); reimbursementMapper.insert(reimb); // 3. 批量插入明细 // 注意设置reimbId外键 dto.getItems().forEach(item -> { ReimbItem entity = new ReimbItem(); entity.setReimbId(reimb.getId()); entity.setName(item.getName()); entity.setPrice(item.getPrice()); entity.setQuantity(item.getQuantity()); reimbItemMapper.insert(entity); }); // 4. 写入审核记录 auditLogService.record(reimb.getId(), userId, "SUBMIT", "提交报销申请", ""); // 5. 触发待审核队列通知 notifyService.notifyAuditor("收到新的报销申请,单号:" + reimb.getReimbNo()); return convertToVO(reimb); }

审核接口的设计思路与此类似,核心变化在于两点:校验当前操作人是否具备对应审核权限、校验报销单当前状态是否允许该操作。权限校验放在Controller层还是Service层取决于项目规范,我倾向于放在Service层,因为有些操作会由系统内部触发而不是HTTP请求。

4.4 前端Vue打包放进Spring Boot的部署套路

前后端分离开发完成之后,部署环节有不少同学卡壳。最省事的部署方式其实是把Vue项目构建出来的静态资源直接放进Spring Boot的static目录里,让Java进程同时充当Web服务器。具体做法分三步。

第一步,在Vue项目的根目录下找到vue.config.js,把publicPath改成相对路径:

module.exports = { publicPath: './', outputDir: 'dist', assetsDir: 'static' }

把publicPath设置为相对路径非常关键,否则打包出来的资源文件默认以根路径开头,放到Spring Boot的static目录后资源全部404。

第二步,执行npm run build,构建完成后会生成一个dist目录,里面包含index.html和static等子目录。把dist里的所有文件复制到Spring Boot的src/main/resources/static目录下。

第三步,启动Spring Boot应用,直接访问“服务器IP:端口/”就能看到前端首页。同时接口路径以/api开头,由Spring Boot自身处理,前后端资源由同一个端口提供服务。

这里有个历史经典问题:前端路由刷新时出现404。原因很简单,Vue是单页应用,前端路由由history模式管理,而Spring Boot静态资源处理时找不到对应的物理文件路径就返回404了。解决办法是在后端写一个简单的转发规则,把非API的请求全部转发到index.html:

@Controller public class PageForwardController { // 注意:该接口要在静态资源处理器之后生效 @RequestMapping(value = {"/", "/login", "/reimb/**", "/audit/**", "/report/**"}) public String forward() { return "forward:/index.html"; } }

5. 常见问题与排查实录

5.1 项目启动失败:Spring Boot版本与JDK不匹配

身边已经不止一个人在这里吃过亏。从官网或IDEA初始化器创建项目时默认选了Spring Boot最新版,本地装的却是JDK 8,启动时直接报错提示“UnsupportedClassVersionError”或“无法访问某些类”。应对这个是很简单的事,创建项目时把Spring Boot版本降到2.7.x,同时保证本地JDK使用8或11,再别在JDK版本上折腾了。

如果你的环境已经装了JDK 17,优先考虑使用Spring Boot 3.x并配好对应的依赖版本。但查资料、找解决方案时,你的搜索结果大多数针对2.x,这点要有心理准备。

5.2 事务不生效的隐形杀手:同类内部调用

在Spring Boot的Bean中,同类里另一个方法直接调用一个带@Transactional注解的方法,事务会静默失效。这里面的原因和Spring Boot默认使用Cglib代理有关。代理对象在方法调用前后额外开启事务、提交或回滚,但内部的this调用绕过了代理对象,事务拦截器根本没机会介入。

典型的失败场景出现在报销单提交逻辑里:submitReimbursement方法内部调用了同一个类的generateNo或者insertDetail方法,而恰好在insertDetail上加了事务注解,那这个事务不会被开启。

解决办法并不难:把需要事务管理的方法拆到另一个Service类中,让业务方法之间通过注入的代理对象交叉调用。千万不要抱侥幸心理,这个坑一旦踩上,数据错乱的问题极难排查。

5.3 前端联调时接口跨域和刷新404

前后端分离开发时,前端跑在Vite默认的5173端口,后端跑在8080端口,前端直接调用后端接口会被浏览器的同源策略拦截。解决办法是在后端加一个CORS配置类:

@Configuration public class CorsConfig { @Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { @Override public void addCorsMappings(Registry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*"); } }; } }

加了CORS之后,还要注意预检请求OPTIONS的处理。如果你写了全局拦截器校验Token,一定要放行OPTIONS请求,否则前端发起非简单请求时会被拦在门外。这个细节排查起来非常隐蔽,因为浏览器控制台报的是跨域错误,实际原因却是拦截器把预检请求给拦截了。

5.4 Maven依赖下载慢与打包太慢

Maven首次构建项目时需要下载大量依赖,网络环境不好的时候能在“Downloading”这步卡十几分钟。最有效的优化就是使用阿里云镜像。在Maven的settings.xml中配置mirror节点:

<mirror> <id>aliyunmaven</id> <mirrorOf>central</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/central</url> </mirror>

另外,如果打包时每次都运行单元测试,可以用mvn package -DskipTests跳过测试环节。Spring Boot项目首次打包要下载spring-boot-maven-plugin等插件,耐心等第一遍跑完,后续增量打包就会快很多。

5.5 MyBatis-Plus分页查询不生效

很多新手用了MyBatis-Plus的分页却发现在SQL日志里没看到LIMIT语句,这是因为没有配置分页插件。在配置类中加一个MybatisPlusInterceptor的Bean就能解决:

@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }

分页插件这个Bean在新版本中还承担了防止全表更新和删除的功能,有一定的保护作用。如果你的项目里故意要做批量删除,记得在拦截器里面配置对应的BlockAttackInnerInterceptor规则,情况不同处理方式不同,这点自己把握好。

6. 个人经验与避坑清单

这套医疗报销系统做完一遍,我自己最大的体会是:业务系统的复杂度不在于技术难度,而在于流程的严谨性和数据的可追溯性。写报销单提交接口很容易,但真正让一个系统像样,拼的是审核记录留痕、金额重新计算、状态机统一流转这些看不见的细节。如果答辩时能把“我为什么在审核表里多设计了一个node字段”讲清楚,整套设计的深度一下子就体现出来了。

最后再分享一个小技巧:项目里所有状态值不要散落成魔法数字,统一放到一个枚举类或数据字典表里管理。比如报销状态,定义成常量PENDING、AUDITING、APPROVED、REJECTED、PAID、CANCELLED,列表页筛选、详情页展示、审核接口判断都引用这一处定义。这样做的好处是,你在写代码的阶段看不出太大区别,但等到联调、改需求、加节点的时候,你会发现所有同步修改都集中在一个文件里,代价极低。对一个毕设或内部系统来说,这就是工程化程度高下的分水岭。

返回列表