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

资讯详情

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

SpringBoot+Vue3+MyBatis城乡居民医保管理系统全栈开发实战

SpringBoot+Vue3+MyBatis城乡居民医保管理系统全栈开发实战

这段时间好几个朋友在问城乡居民医保管理系统这类全栈项目怎么做,正好我手头有一套SpringBoot+Vue3+MyBatis的完整源码跑过业务,把整个开发过程和一些踩坑经验理一理。项目本身不复杂,但是麻雀虽小五脏俱全,涉及参保人员建档、缴费记录、报销审核、统计报表、权限管理这些典型业务,特别适合做毕设、接私活,或者想系统入门全栈开发的人拿来自学。前后端分离这种架构选型,配合MySQL存储,理解透了一整套Web信息管理系统的套路基本就通了。

1. 项目背景与整体设计思路

1.1 这个系统到底要解决什么问题

城乡居民基本医疗保险和我们日常听到的职工医保不太一样。职工医保是单位代扣代缴,个人账户和统筹账户分得清楚;城乡居民医保则是按年度缴费,保障门诊和住院报销,覆盖的是没有固定工作单位的城镇居民和农村居民。这个系统就是为这类业务场景做的信息化管理工具,核心要管好三件事:人、钱、账。

具体拆开来看,业务方需要做参保登记,居民信息要能录入、修改、暂停参保;每年缴费期要批量处理缴费记录,还要能区分个人缴费档次;居民看病后产生报销申请,工作人员要审核报销材料,计算报销金额;最后管理层要看统计报表,比如参保率、缴费率、基金支出情况。这些业务点串起来就是一套完整的管理闭环。

这个系统最适合谁去看、去学?如果你是一个刚接触全栈开发的Java学习者,这套代码的技术栈非常主流。如果你是用作毕业设计,城乡居民医疗信息管理这个选题既接业务地气,又没有特别复杂的行业壁垒,答辩时业务逻辑讲得清楚就是加分项。如果是拿来接私活,当地社区、乡镇卫生院这类单位对这种小系统的需求是真实存在的。

1.2 为什么选SpringBoot+Vue3前后端分离

我见过太多老项目还是用JSP,虽然能跑,但维护起来很遭罪。这个系统之所以采用前后端分离架构,理由其实很简单:前端和后端可以独立开发、独立部署、独立扩展。

后端只提供JSON接口,不关心页面长什么样,前端通过HTTP请求拿数据,渲染交给浏览器。这样做的好处一个是开发效率高,前端用Vue的组件化开发,改页面样式不动后端逻辑。另一个是部署灵活,后端打一个jar包,前端构建出的静态文件扔给Nginx就行。最重要的是,接口化的后端天然适配多端场景,以后要出小程序端、App端,后端几乎不用改。

Vue3相比Vue2最大的变化就是组合式API。写业务逻辑的时候,可以把某个功能相关的响应式数据、计算属性、方法都放在一个setup函数里,不像Vue2的选项式API那样把data、methods、computed强行拆开。对于医疗信息这种一个页面要展示大量关联字段的场景,组合式API的代码聚合度明显更高。

1.3 技术选型的权衡取舍

这套系统的核心选型是:SpringBoot 2.7.x + MyBatis + MySQL 8.x + Vue3 + Element Plus + Pinia + Vue Router,鉴权用JWT。

为什么不选MyBatis-Plus?说实话,我日常写代码是喜欢用MyBatis-Plus的,因为它内置了单表CRUD方法,开发速度快很多。但如果你是做毕设或者想深入理解MyBatis核心原理,建议用原生MyBatis,把Mapper接口、XML映射文件、动态SQL这些底层机制亲手写一遍。这套源码就是用原生MyBatis写的,搞懂它,MyBatis-Plus上手就是半小时的事。

为什么不选Spring Cloud?清醒一点,一个城乡居民医保管理系统,用户量级在几千到几万,单机部署完全够用。引入微服务那一套,Nacos、Feign、网关全上,既增加了部署复杂度,也偏离了这个项目本身该有的学习重点。一个单体应用把代码写清晰、事务边界处理好、SQL索引优化到位,远比空上微服务更具工程价值。

数据库选型也很明确,MySQL在中小型管理系统里的统治地位至今没被撼动,社区资料多、运维经验丰富,Navicat或者官方Workbench都能很好管理。本项目用的是MySQL 8.x,8.0在窗口函数、JSON类型支持上都比5.7更好用。

2. 核心模块拆解与数据库设计

2.1 功能模块划分

做管理系统的第一步不是写代码,而是把模块边界划清楚。我习惯按角色去拆功能,因为不同角色看到的菜单和操作权限是完全不一样的。

管理员负责系统基础配置,比如用户账号管理、角色权限分配、医保政策参数设置(报销比例、缴费档次)。业务经办员负责参保管理,做居民信息的录入、变更、暂停、恢复,以及缴费记录登记。审核员看的是报销申请单,核对费用明细、计算可报销金额。查询统计人员的话主要看各类报表,参保人数变化、缴费进度、报销支出趋势。

菜单结构按这个逻辑走就很清晰:系统管理、参保管理、缴费管理、报销管理、统计报表。数据库表之间的外键关系也是围绕这个模块树展开的。

2.2 数据库表设计要点

数据库设计是整个项目中我花时间最多的地方,表结构设计不合理,后面写SQL全是在还债。这里简单列一下核心表:

  • sys_user用户表,存登录账号、加密后的密码、真实姓名、角色ID。
  • sys_role角色表,用role_code区分admin/operator/auditor/statistician。
  • resident_info参保居民信息表,身份证号、姓名、性别、户籍地址、参保状态、缴费档次等。
  • payment_record缴费记录表,年度、缴费金额、缴费日期、缴费方式、关联居民ID。
  • claim_record报销申请表,关联居民、就诊类型(门诊/住院)、总费用、报销比例、报销金额、审核状态。
  • claim_detail报销明细表,按费用项拆分,比如药品费、检查费、治疗费。

有几个字段设计上的细节要特别强调。

每张表都要有主键,我全部用BIGINT自增ID,不做业务主键。充电线再好看也不如原装线稳定,业务主键一旦后期要调整,会牵连所有外键关系。

居民表里id_card_no身份证号必须加唯一索引,这是业务上的硬约束,同一个身份证号只能有一条有效参保记录。医保业务里重复参保是很敏感的问题。

金额字段统一用DECIMAL(10,2),绝不用FLOAT或DOUBLE。医疗费用涉及钱,浮点数精度丢失那个坑,线上出过一次就是事故。

时间字段用DATETIME,TIMESTAMP虽然省空间但有2038年问题,而且受时区影响,业务系统里统一用DATETIME加应用层默认值,逻辑最清晰。

payment_record表里设置了一个UNIQUE KEY uk_year_resident (pay_year, resident_id)联合唯一索引,确保同一个人同一年度只能有一条缴费记录。这是从数据库层面防止重复缴费的终极兜底。

2.3 关键业务逻辑说明

城乡居民医保报销计算是这个系统的业务核心。报销金额不是简单地"总费用乘以比例",还要看费用科目和起付线。以我这里的实现为例,报销规则是:

  • 门诊报销:年度累计起付线50元,超过部分按60%报销,年度封顶300元。
  • 住院报销:按医院等级区分比例,一级医院报85%,二级医院报70%,三级医院报55%。

这些规则在系统里不是写死在Java代码里的,而是放到数据库配置表insurance_policy中,管理员可以在页面上修改。这样政策调整了,不用改代码重启服务,只需要在后台界面改参数即可。做这类政务、医疗系统的经验就是一条:业务规则要参数化,不要硬编码。

审核流程也值得说。报销申请提交后,状态是PENDING,审核员初审后变为APPROVED或REJECTED,对于金额大的报销单,还需要复审环节。所以claim_record表里有audit_status和second_audit_status两个字段。流程管理是最容易被初学者忽略的,等到业务方说"大额报销单要两个人审",你才发现表结构设计里没想到这层。

3. 后端SpringBoot+MyBatis落地实现

3.1 项目工程结构与分层

后端工程结构我习惯这样组织:

src/main/java/com/example/medical/ ├── common/ -- 统一返回结果、全局异常、常量 ├── config/ -- Spring配置、MyBatis配置、WebMvc配置 ├── controller/ -- 接口层 ├── service/ -- 业务层(含Impl) ├── mapper/ -- MyBatis的Mapper接口 ├── entity/ -- 数据库实体类 ├── dto/ -- 请求/响应对象 ├── vo/ -- 视图对象,用于组合展示数据 └── utils/ -- 工具类(JWT、加密、Excel)

分层的逻辑是:Controller只做参数接收和结果包装,不写业务;Service里放业务规则和事务控制;Mapper只做最简单的数据库读写。我以前见过有人把SQL写在Controller里的操作,短期看少写几个类,等业务复杂起来改一次能掉一层皮。

统一返回结果类Result<T>很关键。所有接口统一返回{code, message, data}结构,前端不用为每个接口单独处理异常格式。code为200表示成功,其他为业务错误码。配合全局异常处理器,任何未捕获异常都能转成友好提示,而不是把堆栈直接甩给前端。

3.2 MyBatis使用的几个关键点

用原生MyBatis,最核心的莫过于XML映射文件。很多初学者只在接口上写@Select注解,这样对简单查询没问题,但涉及动态SQL就很挣扎。我这里的做法是:简单查询用注解,所有复杂的、多表关联的、需要动态条件的查询一律写XML。

拿参保信息分页查询来举例:

<select id="selectPage" resultType="com.example.medical.vo.ResidentVO"> SELECT r.*, u.user_name AS create_user FROM resident_info r LEFT JOIN sys_user u ON r.create_by = u.id <where> <if test="name != null and name != ''"> AND r.name LIKE CONCAT('%', #{name}, '%') </if> <if test="idCard != null and idCard != ''"> AND r.id_card_no LIKE CONCAT('%', #{idCard}, '%') </if> <if test="status != null"> AND r.status = #{status} </if> </where> ORDER BY r.create_time DESC </select>

<where>标签是MyBatis里最实用的东西,它会智能去掉第一个多余的AND,保证拼接的SQL语法正确。#{}预编译占位符必须用,它能防SQL注入,${}那个字符串拼接少用为妙。

MyBatis的缓存机制也值得提一句。一级缓存是SqlSession级别,同一个会话里重复查询会命中断言;二级缓存是Mapper级别,但这里有个经典大坑:如果表数据经常变更,二级缓存没及时失效,就会出现脏读数据。这个系统里缴费和报销都是高频写操作,我直接选择关闭二级缓存,用一级缓存就够用了,避免那些奇奇怪怪的数据不一致问题。

TypeHandler在医保系统里也有用武之地。比如数据库里status字段存的是Integer类型,但Java对象里我想用枚举ResidentStatusEnum,就需要自定义TypeHandler做自动转换。写了TypeHandler之后,Mapper接口里直接用枚举类型作为参数和返回值,代码看着舒服很多。

3.3 事务与数据一致性处理

医保系统对数据一致性要求极高,缴费和报销绝对不能出现"钱收了但记录没了"这种问题。

SpringBoot里事务控制很简单,在Service方法上标注@Transactional注解就行。但要注意几点:

默认事务只回滚RuntimeException和Error,对于Exception的子类比如IOException不会回滚。如果你代码里catch了异常自己处理了,那需要注意业务补偿逻辑。更稳妥的做法是在@Transactional注解里明确指定rollbackFor = Exception.class。

事务失效的经典场景,一是同类内部方法调用,A方法调B方法,B上标了@Transactional也不生效,因为Spring的代理机制只在外部调用时进场;二是方法不是public的。我排查过不少这类问题,经验就是:事务注解规范性地放在public Service方法上,同类内部调用要拆分到不同Bean,或者用TransactionTemplate做编程式事务。

对于缴费这种需要先查后写的操作,我加了一行SELECT ... FOR UPDATE悲观锁:

@Transactional(rollbackFor = Exception.class) public void createPayment(PaymentSaveDTO dto) { ResidentInfo resident = residentMapper.selectByIdForUpdate(dto.getResidentId()); if (resident == null) { throw new BusinessException("参保人员不存在"); } // 检查该年度是否已缴费 int count = paymentMapper.countByYearAndResident(dto.getPayYear(), dto.getResidentId()); if (count > 0) { throw new BusinessException("该居民本年度已有缴费记录"); } paymentMapper.insert(...); }

selectByIdForUpdate会让这条记录的行锁持有到事务提交,两个请求同时提交缴费时,后一个必然等待,从而避免重复缴费。这种机制虽然简单,但在单体应用里相当可靠。

4. 前端Vue3实战要点

4.1 Vue3工程初始化和目录结构

Vue3这一侧我用Vite作为构建工具,创建工程时直接用官方脚手架:

npm create vite@latest medical-web -- --template vue

Vite比Webpack最大的优势就是快,冷启动几乎秒开,开发体验提升非常明显。工程创建完,我习惯把目录整理成下面这样:

src/ ├── api/ -- 按模块封装的接口请求 ├── assets/ -- 静态资源 ├── components/ -- 通用组件 ├── router/ -- 路由配置 ├── stores/ -- Pinia状态管理 ├── views/ -- 页面视图 ├── utils/ -- 工具函数(request封装等) └── App.vue

写接口请求的地方一定要单独放一层,不要在组件里直接写axios.get。为了维护和后端联调,所有接口按模块放在api目录,比如api/resident.js里导出getResidentPage、saveResident之类的方法,组件里调用这些封装好的函数,接口地址集中管理。

axios实例封装是前端最基础也是最重要的工作。我在utils/request.js里统一做了三件事:请求拦截器里把JWT token加到请求头,响应拦截器里统一处理业务码,遇到401跳转登录页,遇到后端返回错误码直接弹出消息提示。

4.2 接口请求与状态管理

状态管理我用的Pinia。Pinia比Vuex轻量太多,没有mutations概念,直接在store里定义state和action,配合组合式API的写法非常顺手。

登录后的用户信息和权限数据放在store里管理:

export const useUserStore = defineStore('user', () => { const token = ref(localStorage.getItem('token') || '') const userInfo = ref({}) const roles = ref([]) function setToken(newToken) { token.value = newToken localStorage.setItem('token', newToken) } function setUserInfo(info) { userInfo.value = info } const isLoggedIn = computed(() => !!token.value) return { token, userInfo, roles, isLoggedIn, setToken, setUserInfo } })

这里有个细节,token一定要持久化到localStorage,否则刷新页面登录态就丢了。当然localStorage有XSS风险,这个项目里我在前端做了基础的输入校验和编码,安全性够用,真正的生产级系统会更倾向用HttpOnly Cookie。

4.3 页面组件设计与权限控制

表单页面我基于Element Plus开发。以参保信息录入为例,主要是一个el-form,配合表单校验规则。身份证号校验可以用Element Plus自带的规则,但为了更严谨,我还写了正则:二代身份证是18位,最后一位可能是数字或X。

权限控制这块,前端做了两层的控制。第一层是路由级:根据用户角色动态生成可访问的路由菜单,管理员能看到系统管理菜单,统计人员只能看到报表页面。第二层是操作级:在页面里用v-permission自定义指令,比如只有审核员角色才能看到"审核通过"按钮。

实现方式其实不复杂。登录接口返回用户角色列表后,前端用router.addRoute动态添加匹配的路由表,然后根据角色生成侧边栏菜单。需要说明的是,前端权限控制只是体验优化,真正安全的后端接口一定要做权限校验。只靠前端藏按钮,懂技术的人直接调接口照样能操作,这种低级失误不能犯。

Vue3组件通信也提一句。我偏爱用defineProps加defineEmits,页面级共享数据放Pinia。那些"props传不下去、emit又传不上来"的问题,大多来自层级太深,解决办法是中间抽一层公共状态进store。

5. 常见问题与排查技巧实录

5.1 后端常见坑

SpringBoot版本不兼容问题。这个问题遇到得多。SpringBoot 3.x要求JDK17以上,把Java 8的用户劝退了一大半。如果你本机是JDK8,老老实实用SpringBoot 2.7.x,配套的MyBatis starter用2.x版本。从Maven中央仓库拉依赖时,版本坐标写错会直接导致启动失败,控制台报一堆ClassNotFound,很多新手在这里卡一整天。

接口返回时间格式问题。后端返回LocalDateTime,前端展示出现"2025-05-01T10:30:00"这种带T的格式。解决方式是在application.yml里全局配置:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

如果字段是LocalDateTime类型,光配date-format不够,还要在pom里引入jackson-datatype-jsr310依赖,并配置JavaTimeModule,否则序列化出来的格式还是不对。

驼峰命名映射缺失。MySQL里字段是id_card_no,Java属性是idCardNo,如果MyBatis没开启驼峰映射,查出来的属性全是null。配置项非常简单:

mybatis: configuration: map-underscore-to-camel-case: true

这个配置我几乎每次写新项目都会检查一遍。

5.2 前端常见坑

跨域问题。本地开发前端在8080端口,后端在8081,axios请求必然跨域。开发环境我在Vite配置文件里设置代理:

server: { proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true } } }

生产环境则交给Nginx做反向代理。关于CORS,有人在后端加@CrossOrigin注解,这个开发环境没问题,生产环境如果前端和后端不同域,需要在Nginx层配合处理,只加注解不一定够。

动态路由白屏问题。使用router.addRoute动态添加路由后,刷新页面发现白屏。原因是路由是在登录后动态加的,页面一刷新,Pinia里的状态没了,动态路由也失效了。解决思路是做一个全局前置守卫,在beforeEach里判断store里有用户信息就走正常流程,没有就尝试调getUserInfo接口恢复登录态,然后再动态添加路由,最后next({ ...to, replace: true })重新进入。这个坑我帮人排查过很多次,本质是路由注册和状态恢复的顺序问题。

5.3 数据库相关问题

MySQL 8.x连接报SSL错误。用Connector/J 8.x连MySQL时,控制台报The server certificate is not trusted这类SSL错误。解决方案是连接URL上加参数:

jdbc:mysql://localhost:3306/medical_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8

serverTimezone必须指定,否则连8.x数据库默认时区不对会报错。字符集也一定要指定UTF-8,否则中文乱码后患无穷。

大批量缴费数据导入性能。表格批量导入居民缴费记录时,一条条插入太慢。我的优化思路是改用MyBatis的批量插入,在Mapper XML里用foreach拼接批量SQL,一万条数据,批量插入比逐条插入快几十倍。注意MySQL单条SQL有max_allowed_packet限制,数据量特别大时要分批,每批500条左右比较稳。

<insert id="batchInsert"> INSERT INTO payment_record (resident_id, pay_year, pay_amount, pay_date, pay_type) VALUES <foreach collection="list" item="item" separator=","> (#{item.residentId}, #{item.payYear}, #{item.payAmount}, #{item.payDate}, #{item.payType}) </foreach> </insert>

慢查询排查。报表统计页面在数据量多了之后明显变慢,我用EXPLAIN查看执行计划,发现是claim_record表的resident_id字段没建索引,全表扫描导致慢查询。后来给所有外键字段补了索引,再配合覆盖索引查询,报表接口从秒级降到了毫秒级。索引对数据库性能的提升是立竿见影的,但是也别过度建索引,写多读少的表索引太多反而拖慢写入。

5.4 部署与运维经验

打包部署我用的是Maven的package命令,后端打成可执行jar,前端用npm run build生成dist目录。最终部署结构是:Nginx托管前端静态文件,同时反向代理/api请求到Java服务,MySQL单独一台机器或同机部署。

有一个细节,jar包启动时默认内存使用较大,可以在启动命令里限制:

java -Xms256m -Xmx512m -jar medical-system.jar --spring.profiles.active=prod

生产环境的数据库连接、文件路径等配置放到application-prod.yml里,用spring.profiles.active切换,不要把生产库密码写进仓库,这个习惯一定要养成。

日志方面,用Logback按天滚动,保留30天日志。排查线上问题时,日志就是最后的救命稻草。我用@Slf4j在Service层关键方法里打日志,入参、出参、异常信息都记录,出了问题能快速定位到具体环节。

6. 一点想说的实践经验

跑完这套系统,我个人最大的感触是:全栈项目的坑永远都在你以为已经搞定的地方。

后端接口用Postman测试全通过,以为万事大吉,结果前端一联调就暴露了字段命名不一致、格式不兼容的一堆问题。所以我现在做项目坚持一个流程:每个接口开发完,后端先自测,然后立刻跟前端联调,不让问题过夜。顺手把MyBatis打印执行的SQL打开,上线排查时能看清每条SQL实际是什么样子的。

这个系统后续如果要扩展,建议往这几个方向走:接入Excel导出功能,把参保名单和缴费清单导出成报表,这个需求几乎一定会来;增加消息通知模块,缴费期到了给居民推送提醒,用WebSocket或者简单的邮件通知都行;把报销审核流程加一个会签功能,让多级审核能够并行处理。

从技术层面看,SpringBoot+Vue3+MyBatis这套组合在中小型管理系统中确实能打。学习的时候不要光看视频,真正把源码自己跑起来,从改一个接口、加一个页面开始,慢慢把整个流程走通,你对前后端分离、数据库设计、权限控制这些概念的理解会完全上一个台阶。

返回列表