1. 项目概述与业务逻辑拆解接触过不少毕业设计选题和中小型企业内部系统搭建的案例医疗报销系统几乎是最能锻炼完整业务闭环能力的项目之一。表面上看它就是一个“提交报销单、领导审批、财务打款”的简单流程但真等到你动手设计表和接口的时候会发现里面埋着不少坑多级审核状态怎么流转、报销金额怎么按规则计算、单据里那些五花八门的明细怎么归类、不同角色看到的操作按钮如何区分这些都不是CRUD能直接糊弄过去的。而这个“基于SpringBoot的医疗报销系统”恰恰把所有关键问题都串在了一起同时技术栈主流、成本低、演示方便非常适合做毕设项目或内部管理工具的练手原型。我把它拆开看核心解决的就是三件事让患者或员工能快速提交医疗费用单据让审核人员能规范、可追溯地完成审批让财务和管理层能实时看到资金使用与报销的统计数据。这里面的“报销”不只是填个数字它牵扯到费用明细、政策规则、审核留痕、状态变更、消息提醒等一连串动作本质上是一个轻量级的BPM业务流程管理系统只不过业务流程长在了医疗报销这个具体场景上。这个系统适合谁来参考一类是正在挑毕设题目的计算机专业学生另一类是想快速给公司或社区做一个报销管理工具的后端开发。前者需要的是完整、能答辩、能演示的功能闭环后者需要的是流程清晰、能扩展、能上生产的工程结构而这套基于Spring Boot实现的项目方案刚好能同时覆盖两边的诉求。我为什么要说“基于Spring Boot”是这个项目成立的前提因为Spring Boot把SSMSpring 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?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/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.StdOutImpl4.2 登录认证与权限拦截JWT 拦截器的经典组合医疗报销系统的所有业务接口都不能裸奔必须先解决登录认证。我采用的方案是用户登录成功后后端生成一个JWT令牌返回给前端前端在后续请求的Header中携带该令牌后端通过拦截器统一校验。先生成一个JWT工具类核心方法包括生成Token和解析Tokenpublic 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.htmlController 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 idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://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列表页筛选、详情页展示、审核接口判断都引用这一处定义。这样做的好处是你在写代码的阶段看不出太大区别但等到联调、改需求、加节点的时候你会发现所有同步修改都集中在一个文件里代价极低。对一个毕设或内部系统来说这就是工程化程度高下的分水岭。