城乡居民医保系统实战:SpringBoot+Vue3前后端分离开发全解析 这段时间刚把手头一个城乡居民基本医疗信息管理系统的完整源码整理出来正好是这个圈子里的经典组合后台Java SpringBoot前端Vue3持久层MyBatis数据库MySQL前后端完全分离。我这人比较实在先把话说清楚——这就是个典型的Java全栈业务系统你要是正在学SpringBootVue3想找实战项目、或者毕业设计想直接抄一套能跑通前后端的源码、又或者刚入职要接手类似的医疗管理系统这篇文章都能给你省不少事。我尽量把设计思路、表结构、前后端联调的关键点、还有我实际踩过的坑一次性讲透。不是那种光贴代码不解释的教程也不是上来就让你复制粘贴的“快餐文”我会把每一步为什么这么做都在旁边标注清楚方便你后续自己改、自己扩展。1. 项目定位与技术选型为什么是这套组合1.1 业务场景先想明白城乡居民医保系统到底管什么动工之前我习惯先把业务方的话翻译成模块清单。这个系统的服务对象是城乡居民核心业务绕不开三件事参保人信息怎么管、缴费记录怎么记、报销流程怎么走。除此之外还有经办机构、业务员账号、公告通知这类支撑性功能。我整理出来的核心模块大概是以下这些居民信息管理家庭成员档案、身份信息、参保状态。缴费管理按年度缴费、补缴记录、政府代缴标注。报销管理门诊报销、住院报销、大病补助三类场景。定点机构管理医院、卫生院的目录维护。用户与权限管理员、业务员、普通居民的三种角色。统计报表参保率、基金支出等常用汇总数据。每个模块之间是有依赖关系的。居民先建档才能去缴费缴费之后产生的报销记录才能关联到人。所以数据库设计的时候居民表是根节点缴费和报销都外键指向居民信息这层关系捋清楚后面写SQL才不会乱。这个需求画像直接决定了技术方案。它不是高并发电商系统不需要Redis集群和消息队列堆上去但它有复杂的业务状态流转、大量的条件查询、以及政府类项目常见的报表导出需求。这种系统用SpringBootMyBatis就是最顺手的搭配SpringBoot负责快速搭建和集成MyBatis用动态SQL去迎接业务上“每个区县查询条件都不一样”的现实。1.2 SpringBoot、Vue3、MyBatis、MySQL这四个选谁背后的取舍先看后端框架。SpringBoot现在基本是JavaWeb项目的默认起点内嵌Tomcat、自动配置、Starter机制可以把以前SpringMVC时代那一堆XML配置全部干掉。我这个项目用的是SpringBoot 2.7.x版本为什么不用3.x因为3.x是基于Jakarta EE的部分老牌MyBatis周边插件升级适配没跟上我不想在兼容性上浪费时间2.7足够稳定。后面你如果自己搭项目也建议先看依赖支持情况再决定上不上新版。Vue3这边最大的变化就是组合式API。我项目里用的是script setup语法加TypeScript写起来比Options API清爽太多。状态管理用Pinia路由用Vue Router 4UI组件库用Element Plus。这套组合在目前国内的中后台项目里占比非常高原因很简单——文档全、坑少、招人好招。MyBatis和MySQL这对组合不用多说。MyBatis比JPA更容易控制SQL尤其适合报表类和复杂查询场景。MySQL则是开源数据库里受众最广的成本为零运维资料遍地都是。这个项目规模下MySQL单库扛住几千个并发业务操作完全不是问题。1.3 前后端分离到底图什么我这个系统是严格前后端分离的后端只出JSON接口前端用Nginx托管静态页面运行的时候通过/api前缀代理转发请求。这么做的直接好处是前端和后端可以并行开发我后端还没写完前端同事已经用Mock数据把页面调通了。另外一个容易被忽略的好处是部署维度。后端打包成jar丢到服务器前端build完扔到Nginx或者OSS上互不影响。万一以后要做小程序端或者App端后端接口完全不需要改直接复用。你要做毕设或者小公司项目这套架构拿出去也说得上话面试的时候还能顺势聊聊怎么解决跨域和部署问题。提示前后端分离后跨域问题几乎是必踩的坑。开发环境我用Vite的代理解决生产环境靠Nginx反向代理后端代码里反而是不需要开启全开CORS的上线会更安全。2. 核心业务模块拆解与数据库设计2.1 业务模块边界划分与主流程串联模块设计我建议按“参保生命周期”来拆而不是按部门职能来拆。一个居民从登记、缴费、报销到退保这条主线串起来系统里所有表都能找到归属。这样设计的好处是后续加需求比如新增一个“异地就医备案”功能的时候你能快速判断它应该挂在哪个模块下。主流程大概是创建家庭档案 → 添加家庭成员 → 发起年度参保缴费 → 产生报销申请 → 审核报销 → 记录打款。整套流程里涉及状态字段的地方我做了一个状态机约定比如缴费记录有“待支付、已支付、已退款”报销记录有“待审核、审核通过、已打款、审核驳回”。每个状态只允许往固定的下一个状态流转代码里用常量池统一管理不乱用魔法数字。前面说的动态SQL在报销查询里就派上大用场了。业务员经常要根据姓名、身份证号、缴费年份、报销类型、审核状态这五六个条件自由组合筛选。用MyBatis的where加if标签既能过滤无效参数又能避免出现“WHERE 11”这种尴尬SQL代码可读性也好很多。2.2 关键表结构设计与字段说明数据库我最终建了十来张表这里挑四张最能体现业务逻辑的来展开讲。居民信息表resident_info这是整个系统的根基。主键用自增id业务上唯一键是id_card_no身份证号。核心字段包括姓名、性别、出生日期、户籍地址、手机号、参保状态。实际开发里我额外加了一个family_id把同户口本的人归到同一个家庭组这样缴费和报销的时候可以按家庭维度汇总。缴费记录表payment_record关联resident_id和year。这里有个设计经验同样的居民和年份最好加唯一约束uk_resident_year防止业务员手抖点了两次新增导致一条居民一年有两条缴费记录。金额字段用DECIMAL(10,2)全程不用float。备注字段留一个payment_type区分“正常缴费”和“政府代缴”这对后期统计财政补助很有用。报销记录表reimburse_record是所有表里最难设计的。因为它要兼容门诊、住院、大病三套不同的报销规则所以我把公共字段申请时间、就诊类型、总费用、报销金额、审核状态放主表把差异化数据放到一个JSON类型的extra_info字段里。MySQL的JSON字段在配合MyBatis的时候需要在TypeHandler上做处理前面提到的TypeHandler实践正是给这种场景用的。用户权限表我用了经典的RBAC模型sys_user、sys_role、sys_user_role三张表。居民端登录用一个user_type字段区分后台业务员和普通居民前端根据角色动态渲染菜单。单独说一句密码存储必须用BCrypt加密别用MD5哪怕项目再小也不能偷懒。2.3 数据一致性事务和并发控制不能省医保系统的数据直接涉及资金一致性是底线。我用三层手段做保护第一层所有涉及写操作缴费、报销审批的Service方法都加上Transactional保证方法内多表操作要么全成功要么全回滚。第二层关键查询用数据库行级锁。比如审批报销的时候先从reimburse_record表用SELECT ... FOR UPDATE锁定这条记录再更新状态。如果不加锁两个审核员同时操作同一条记录就可能出现状态覆盖这在资金类系统里是大忌。第三层用乐观锁兜底。在设计表的公共字段里放了一个version字段初始为0每次更新自增UserService用MyBatis的updateById配合XML里的WHERE version #{version}更新返回0条就提示前端“数据已被修改请刷新后重试”。这种兜底策略对并发量不大但准确性要求高的政务类项目特别实用。3. 后端落地SpringBootMyBatis的工程化实操3.1 分层架构和目录设计一眼能看懂的工程结构后端目录我按“Controller → Service → Mapper”经典三层划分但加了一层DTO和VO隔离。Controller层接收的请求体走DTO返回给前端的统一走VO实体类Entity只活在Service和Mapper里。这样做的原因是数据库表字段有时候不能直接暴露给前端比如resident_info表里有政治面貌、户籍编号这种内部字段就不该出现在居民端查询结果里。工程结构大致长这样com.project.medical ├── controller # 接口层只做参数接收和结果包装 ├── service # 业务逻辑层事务注解加在这层 ├── mapper # MyBatis的Mapper接口 ├── entity # 数据库实体类 ├── dto # 接收参数的模型 ├── vo # 返回给前端的模型 ├── config # 配置类跨域、拦截器、MybatisPlus配置 ├── common # 统一返回结果、异常处理、常量池 └── utils # JWT工具、日期工具等3.2 MyBatis动态SQL与TypeHandler的实战这个项目没有用MyBatis-Plus而是用的原生MyBatis原因是我对SQL的控制欲比较强加上系统里大量汇总报表需要手写多表关联原生MyBatis的XML映射更加直观。你要是熟悉MP也能用但建议核心报表SQL仍然手写这样方便DBA后续做SQL优化。动态SQL最典型的场景是居民分页查询我贴一段实际在用的代码select idselectResidentPage resultTypecom.project.medical.vo.ResidentVO SELECT r.id, r.name, r.id_card_no, r.phone, r.insurance_status, f.family_name FROM resident_info r LEFT JOIN family_info f ON r.family_id f.id where if testkeyword ! null and keyword ! AND (r.name LIKE CONCAT(%, #{keyword}, %) OR r.id_card_no LIKE CONCAT(%, #{keyword}, %)) /if if teststatus ! null AND r.insurance_status #{status} /if /where ORDER BY r.create_time DESC LIMIT #{offset}, #{pageSize} /select这里LIMIT的offset在Service层算好没有引入额外分页插件数据量不大的场景下这种手写分页够用还少一个依赖。MyBatis拼接SQL的坑在于大括号和转义如果筛选条件是“小于号”在XML里必须写成lt;这个细节在“状态筛选的创建时间是某个时间之前”这种查询里经常踩雷我后面会在问题排查部分专门说。TypeHandler我做了两个自定义实现。第一个是处理MySQL的JSON字段将数据库返回的JSON字符串转成Java实体类用Fastjson2的JSON.parseObject反向写入时再把对象序列化回JSON字符串。第二个是处理枚举类型比如报销类型字段在数据库存的是HOSPITAL这种字符串到Java里直接映射成枚举对象比到处写if-else判断强得多。3.3 统一返回体、全局异常拦击、JWT鉴权一个都不能少前后端对接最怕的就是接口各写各的格式。我规定所有接口返回同一个结构{ code, message, data }成功时code是200业务异常时code是具体负数全局异常处理器统一捕获并转换成规范JSON。前端Axios响应拦截器只看code等于在前后端之间定了契约。全局异常处理这块我建议在项目初期就做好不要等接口多了再加。我的GlobalExceptionHandler里处理了参数校验异常、业务异常、权限不足异常和兜底的系统异常每类异常返回给前端的message都不带堆栈信息只给用户能看懂的提示内部日志里才记完整错误栈。接口鉴权用JWT登录成功后后端返回一个有效期为24小时的Token前端存在Vuex/Pinia里并在Axios请求头携带。我的拦截器逻辑很简单从请求头取Token校验签名和过期时间把解析出的用户id塞到Request attribute里后续业务代码可以直接取当前操作人。白名单接口登录、验证码、居民端公开查询在配置类里放行其余全部拦截。这里有个容易忽略的点JWT密钥必须放在环境变量或配置中心不要明文写在代码里。我这个项目里Key是从application.yml外部读取的部署时通过--spring.config.additional-location加载外部配置这样服务器上还能单独配一套代码仓库里不出现真实密钥。4. 前端实现Vue3Element Plus的开发实录4.1 工程初始化用到哪些东西前端我用Vite搭建工程选择Vue3TypeScript模板一路默认创建。Vite相比Webpack最大的体会是启动速度快开发时热更新几乎无感这在频繁调试后端接口时很省时间。工程里主要依赖就这几个vue-router、pinia、element-plus、axios、sass。我习惯在src目录下按功能分模块而不是按文件类型分。比如src/views/system放系统管理相关页面src/views/insurance放居民和缴费页面src/api下建模块级API文件。这样后期维护时一个业务模块涉及的所有文件都在相邻位置效率比“把所有组件堆积在components”高一大截。4.2 登录鉴权与路由守卫用户没登录就把人送回登录页前端登录流程是这样登录页拿到用户名密码调/auth/login接口把返回的Token存到Pinia和localStorage然后调一次/auth/userinfo拿用户角色和菜单权限前端根据权限数组动态注册路由。首页刷新的时候从localStorage恢复Token再根据当前页面路径校验是否有权限。路由守卫必须写而且不能只在前端拦。我的做法是router.beforeEach里在没有Token的时候跳转登录页有Token但没角色信息时去拉用户信息拉回来正常放行。这一步要做好时序控制否则会出现刷新页面白屏的bug。实际项目里我还遇到过一种情况后端返回的某个用户角色标记是超管前端就应该显示全部菜单如果不是超管菜单得按权限过滤。这个逻辑放在路由生成器里所有菜单都根据权限字段筛选后再加入路由表。4.3 Axios封装和页面的三驾马车表格、表单、弹窗Axios我封装的核心在于请求拦截器和响应拦截器。请求拦截器统一从Pinia里取Token往header里塞响应拦截器统一处理错误码。这里有业务规范后端返回的code是200时直接返回data给页面如果是401就清空本地Token并跳转登录页其他错误码弹出Message提示内容页面代码不需要到处写try-catch。页面实现层面Element Plus的el-table配合el-pagination是这类管理系统的绝对主力。我封装了一个CommonTable.vue组件把搜索条件、表格列、分页参数作为props传入内部统一管理请求loading和分页事件。写业务页面的时候只需配一个数组描述表格列再加一条查询函数基本不用重复造轮子。注意Element Plus的表格列渲染有多种方式简单的直接prop映射复杂状态比如“报销状态”需要显示成Tag标签的用formatter或插槽。如果你用插槽建议命名规范统一比如#status{ row }”能省很多查文档的时间。4.4 居民端页面移动端自适应的一个简化方案这个系统除了后台管理还有一个给普通居民用的简易端主要功能是查缴费记录、提交报销申请。我没让小前端单独写一套移动端页面而是用Vue3的响应式布局配合el-card和栅格系统把电脑端和手机端的体验都照顾到了。技术上核心是几个断点判断因为业务相对简单不需要上vant这种移动端组件库。手机端最核心的交互是拍照上传报销票据。前端把图片压缩后转成Base64传后端后端用OSS或者本地磁盘存储。这里提醒一个实操细节图片传后端前要先压缩尤其是手机摄像头拍出来的照片动辄5MB以上不压缩的话上传很慢后端带宽也吃紧。我在前端用Canvas做了压缩长边压到1600px再转JPG体积能砍掉一半以上。5. MySQL部署与环境联调那些配置文件和脏活累活5.1 数据库初始化与配置文件的关键点建库我建议用utf8mb4字符集不要用老旧的utf8。原因是utf8在MySQL里最多存3字节碰到生僻字或者表情符号就报错utf8mb4完全没这个问题。我的建库语句是这样的CREATE DATABASE medical_insurance DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;配置文件方面最关键的三个参数是驱动、连接池、时区。MySQL 8.x的驱动类名是com.mysql.cj.jdbc.Driver连接串上一定要带serverTimezoneAsia/Shanghai否则你在Java里传的LocalDateTime和数据库存的时间经常对不上差8小时是常见问题。连接池用HikariCP具体配置我贴在下面。spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/medical_insurance?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: ${DB_PASSWORD} hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000useSSLfalse和allowPublicKeyRetrievaltrue这两个参数我建议直接加上。新装MySQL 8.x默认可能开启SSL相关特性不处理会在连接时报一堆警告甚至直接失败。本地开发可以关SSL生产环境如果有加密传输需求再反着配置就行。5.2 前后端联调的三种“不响”问题怎么排查联调阶段最容易遇到三类问题第一种是后端接口测通了前端调不通第二种是前端报错401第三种是能通但数据是null。这三种问题各有各的排查思路。后端接口通但前端不通先打开浏览器DevTools看Network面板如果请求是OPTIONS预检请求就直接计入第一种跨域问题。前端的解决办法是Vite配置server.proxy后端是SpringBoot配置CorsFilter二选一即可不要两边都开那种“后端也配了前端也配了”的反而容易出双写头的矛盾。401问题九成是Token没有正确传递。检查点有三个登录成功的返回值是不是取了正确的字段Axios请求拦截器是不是把Token塞进了Authorization后端的JWT过滤器放行白名单路径是不是把需要鉴权的路径也放行了。这三个点逐个排查马上就能定位。数据null的问题大多出在MyBatis映射上。如果数据库字段是下划线命名insurance_statusJava实体没有配map-underscore-to-camel-case: true映射出来就是null。这个配置在application.yml里加一行就好但凡用过原生MyBatis的都该养成这个习惯。6. 常见问题速查表与个人经验沉淀我把这次实操过程中高频问题整理成了一个速查表方便你复制过去贴到项目文档里问题现象排查方向解决办法MySQL连接失败提示SSL错误检查连接串SSL参数加useSSLfalseallowPublicKeyRetrievaltrue接口数据中文乱码检查数据库、连接串、页面编码统一utf8mb4连接串加characterEncodingutf8前端请求404但后端接口存在检查代理地址和网关路径/api前缀统一Nginx或Vite代理路径保持一致保存数据报“数据已被修改”乐观锁版本号不匹配刷新页面重新加载不要直接覆盖旧数据JWT过期提示不明确响应拦截器没有处理401统一拦截401并跳转登录页日期字段返回格式奇怪Jackson序列化配置缺失加spring.jackson.date-format或全局配置ObjectMapperMyBatis报SQL语法错误XML里用了特殊字符小于号、大于号用lt;和gt;转义上传图片提示文件过大前端未压缩或后端限制太小前端Canvas压缩后端调大max-file-size配置最后说几点我自己沉淀下来的经验。做这类“信息管理系统”项目不管技术多花哨业务正确性永远是第一位的。医保系统的状态流转、金额计算、人员身份校验每一处都直接影响真实用户代码里该写的注释、该加的日志、该做的入参校验偷懒一时爽上线火葬场。另外一个小建议项目初期就把日志框架用好方法入口打印入参、耗时报错的时候打印堆栈排查问题能快非常多不要等出了事故才开始补日志。再分享一个关于接口设计的小经验所有涉及金额变更的接口一定要在返回参数里带服务端的“当前时间”和“操作流水号”这样哪怕前端页面异常用户也能对着流水号找客服溯源。这套系统做完我自己对SpringBootVue3的工程化落地又加深了一层理解代码全部整理在源码里有需要研究表结构和接口设计的朋友直接对照着跑起来看比看多少文章都有用。