
这个题目乍一看有点唬人又是智能、又是家庭、又是医疗保险。其实拆开来看它就是一个非常典型的 Java Web 信息管理系统——只是把传统单用户医保系统换成了“以家庭为账户单位”的业务场景再加一点规则计算和提醒功能把“智能”两个字坐实。用 Java SpringBoot 做后端前端走 Web 浏览器访问核心价值就是把家庭成员的医保信息、缴费记录、就诊费用、报销审核全部串起来做成一个可运行、可演示、可扩展的完整平台。这篇博文我按我实际带毕设的惯用思路来写不绕弯子直接讲怎么搭、怎么算、怎么防坑。内容定位是给做计算机毕业设计的同学当一个可参照的“从零到答辩”行动手册。你如果是第一次做完整 Web 项目会在这里看到数据库怎么建、报销金额怎么算、登录权限怎么拦、文件发票怎么传如果你已经有基础可以直接跳到第 6 章看常见坑很多问题我在帮别人调项目时反复碰到。1. 选题分析与整体定位1.1 这个系统到底解决什么问题家庭医疗保险这个场景和普通的“个人医保报销系统”差异其实很大。个人版的核心是一人一档、一人一保而家庭版天然带一种“账户共享”的味道一个家庭里可能有老人、小孩、在职成年人每个人的报销比例不同、额度不同但保费可能是一起缴的报销进度也可能共享同一个家庭年度总额度。所以这个系统要覆盖的最基本业务链条是创建家庭档案 → 录入家庭成员 → 选择/购买保险方案 → 产生就诊记录与医疗费用 → 提交报销申请 → 系统自动核算报销金额 → 审核人员确认 → 核销额度并生成报表。这就是一条很完整的业务主链路。把这条链路用 Web 系统落地本质上你做的就是一个 MIS管理信息系统。这和图书管理、宿舍管理的底层套路是一样的CRUD 状态流转 统计报表。但好在业务对象换成“医保”之后选题看起来比图书管理系统有说服力得多论文也好扩展。1.2 面向的用户角色与核心场景系统里至少要有三类角色这也是你划权限、画用例图的基础家庭成员家庭维度用户登录后只能看自己家庭的数据可以添加就诊记录、提交报销申请、查看报销进度、下载报销单。审核管理人员一般市级/区级审核窗口角色能查看所有家庭的申请做通过或驳回操作。系统管理员维护保险方案、医院等级配置、报销比例参数、公告信息以及管理账号。一个更贴近现实的场景是家里老人生病住了院产生了一笔住院费用户主在家里的电脑上把这些费用录入系统上传发票照片提交报销申请系统按“该成员的保险方案医院等级费用类型”自动算出报销金额审核人员在管理端核对发票后点通过系统把报销金额累加到该家庭的年度已报销额度里同时检查是否触发 80% 额度的预警提醒。这就是一套完整的“智能感”闭环。1.3 “智能”二字落在哪里毕设题目里的“智能”经常被导师追问不要只说“加了人工智能”那样反而容易被问住。合理做法是把“智能”落地成可解释的业务规则智能核算报销金额不需要人工笔算系统按配置好的比例和限额自动计算。额度预警当家庭年度报销额达到额度的 80% 时自动提示“本年度剩余报销额度紧张”。规则可配置医院等级、费用类型、报销比例都做成配置表管理员调整参数后新申请自动按新规则核算。这三件事每件都对应明确的数据库字段和代码逻辑答辩的时候你能把“智能”落在具体方法上导师就会觉得这个系统是完整设计过的不是套个壳子。2. 技术选型与架构设计2.1 为什么是 SpringBoot 而不是 SSH/SSM现在做 Java Web 毕设SpringBoot 基本是默认答案。理由很朴素它把繁琐的 XML 配置去掉了内嵌 Tomcat 让你不用再往本机装外部容器打一个 jar 包就能跑这对不熟悉服务器部署的同学非常友好。版本选择我建议优先考虑 SpringBoot 2.7.x 配 JDK 8。不少学校的实验环境和答辩电脑还停留在 JDK 8少数学校有 JDK 11。如果你一上来就选 SpringBoot 3.x最低要求是 JDK 17部分老机器的环境会出问题跑不起来的时候你分的清是代码问题还是 JDK 版本太高吗没必要给自己增加这种不确定性。我自己给同学改项目默认就是 SpringBoot 2.7 JDK 8 MySQL 5.7/8.0一套组合从大二用到研三都不会出错。持久层选 MyBatis-Plus 而不是纯 MyBatis 或 Spring Data JPA。理由是单表 CRUD 不需要手写 SQL它内置的 LambdaQueryWrapper 写起来非常流畅分页插件也好用一个配置类搞定。核心业务里只有查询统计需要手写 SQL量不大。2.2 前后端分离还是服务端渲染这里有两套主流方案方案 A推荐给有点前端基础的同学后端 SpringBoot 提供 RESTful API前端 Vue 3 Element Plus配合 Vite 和 Axios 做前后端分离。好处是项目结构看起来架构感更强论文里的架构图好画代码量也更像一个“平台级”系统。方案 B推荐给完全不想碰前端构建工具的同学后端用 Thymeleaf 模板引擎页面由服务端渲染。好处是只有一个工程本地启动后浏览器直接访问不容易出现跨域、打包这类工程化问题。缺点是交互体验一般写复杂表单和动态表格要用 Bootstrap 或原生 JS 顶一下。我见过不少同学卡在“前端打包后接口 404”“CORS 跨域报错”这类问题上浪费三四天。如果你对 npm、Node、Vite 没把握选 Thymeleaf 也不丢人系统照样完整如果你想让答辩时项目观感更现代那就选 Vue3 前后端分离但务必预留一周时间专门做联调。2.3 文件存储方案MinIO 要不要用报销申请一定要上传发票图片或 PDF 文件这就涉及文件存储。最简单的是存本地磁盘配置文件里指定一个 upload 目录上传成功后把访问 URL 存进数据库再写一个静态资源映射让上传的文件可以通过 /files/** 访问。这套方案在毕设里完全够用。如果你想让技术栈更丰满可以引入 MinIO。MinIO 是一个兼容 S3 协议的对象存储服务部署方式很简单本地下载安装启动默认端口 9000控制台 9001。SpringBoot 里引入 minio 的 Java SDK配置 endpoint、accessKey、secretKey、bucketName上传时直接用 putObject返回文件名拼成访问地址。加了这个东西论文里可以写“使用现代对象存储统一管理医疗票据文件”评审观感确实会好一截。但提前说清楚MinIO 在答辩现场如果没启动报销票据模块就全挂了所以部署演示前一定要检查服务状态。核心依赖清单大致是这样dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdcom.auth0/groupId artifactIdjava-jwt/artifactId version4.4.0/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdio.minio/groupId artifactIdminio/artifactId version8.5.7/version /dependency dependency groupIdcom.github.xiaoymin/groupId artifactIdknife4j-openapi2-spring-boot-starter/artifactId version4.4.0/version /dependencyJWT 我选的是 java-jwt 而不是 Spring Security 复杂的过滤器链。对毕设场景Security 全链路配置太重而且一旦配错会拦掉所有接口导致 401排查起来比较劝退。直接用拦截器校验 JWT一个小工具类生成和解析 Token代码量少、逻辑清晰也好讲。3. 数据库设计与核心表结构3.1 核心表清单与字段设计这是整个系统的地基。我按一个相对简单但完整的方案来设计一共 8 张核心表。别嫌表多真正落过地就知道这些字段一个都省不下来。表名核心字段说明family 家庭表id, family_no, family_name, address, head_member_id, created_time家庭账户主体family_no 唯一编号member 家庭成员表id, family_id, name, id_card, gender, birth_date, relation, phone, member_type家庭成员档案relation 表示户主/配偶/子女/父母user 用户账号表id, member_id, username, password, role, status登录账号和成员一一对应角色区分用户/审核/管理员insurance_plan 保险方案表id, plan_name, annual_limit, reimburse_rate, hospital_level, coverage_type, status方案定义存年度额度、默认报销比例、适用条件policy 参保记录表id, member_id, plan_id, start_date, end_date, premium, status成员和方案的多对多关系存参保起止期和保费medical_record 就诊记录表id, member_id, hospital_name, hospital_level, diagnosis, total_fee, medical_date, fee_type就诊明细费用类型区分门诊/住院/药品claim 报销申请单表id, claim_no, family_id, applicant_id, total_fee, total_reimburse, status, audit_remark, apply_time, audit_time报销主表一条申请对应多个就诊明细claim_item 报销明细表id, claim_id, medical_record_id, fee, reimburse_amount, rate申请与就诊记录关联存每条的核算结果几个必记的字段设计原则金额一律用 DECIMAL(12,2)绝不用 float/double。报销金额涉及精确计算二进制浮点数带来的精度误差在财务数据上是不可接受的。状态字段用 tinyint 或 varchar 存固定枚举值比如报销状态 0 待提交 1 待审核 2 审核通过 3 已驳回 4 已核销。代码里用常量类或枚举类统一维护不要散落在业务代码里写魔法数字。所有表都加 create_time 和 deleted 字段。deleted 用逻辑删除 0/1这样统计历史数据时不会因为物理删除导致账目对不上。3.2 表关系与业务约束表关系其实不复杂核心是几个一对多一个家庭有多个成员所以 family 与 member 是一对多一个成员可以多次参保不同方案所以 member 与 insurance_plan 通过 policy 建立多对多一个成员有多条就诊记录所以 member 与 medical_record 是一对多一张报销主单包含多条明细所以 claim 与 claim_item 是一对多。真正容易忽略的是约束。比如同一张发票同一笔医疗记录不能被重复报销参保状态为已过期时不能提交报销报销金额不能超过该方案剩余额度。这些约束不写在数据库外键里而是在 Service 层通过查询判断来拦截。数据库外键我建议不要建MySQL 外键在很多真实项目里反而会导致删除时的锁问题用代码逻辑保证一致性就够了。3.3 关键统计 SQL家庭年度报销额度计算额度预警是“智能”的核心体现代码最终要落到 SQL 上。比如查某个家庭在指定年度内累计已报销金额SELECT IFNULL(SUM(c.total_reimburse), 0) FROM claim c WHERE c.family_id #{familyId} AND c.status 4 AND YEAR(c.audit_time) #{year} AND c.deleted 0这里的 status 4 表示已经完成核销只有核销通过的钱才真正计入额度。如果你把“待审核”的钱也算进去那额度预警就不准了。这个细节我在代码评审时帮同学揪出来过属于典型的“逻辑边界不清晰”。统计报销明细时另一个常用 SQL 是按参保成员分组统计SELECT m.name, COUNT(mr.id) AS cnt, SUM(mr.total_fee) AS total_fee FROM medical_record mr JOIN member m ON mr.member_id m.id WHERE mr.family_id #{familyId} GROUP BY m.id ORDER BY total_fee DESC这类 SQL 建议单独写在 Mapper XML 里不要全部堆在 MyBatis-Plus 的 Wrapper 上。一眼能看懂的统计 SQL比满屏 QueryWrapper 嵌套可维护得多。4. 后端核心功能实现与关键代码4.1 登录鉴权与行级数据权限登录流程是标准的用户提交用户名密码 → 后端校验 → 签发 JWT Token → 前端后续请求在请求头携带 Authorization: Bearer xxx。JWT 的载荷里我一般只放 userId、memberId、familyId、role 这四个必要字段不塞敏感信息。关键点是“行级权限”。家庭成员登录后不能看别人的家庭数据。最简单粗暴又有效的方案接口根据 Token 里的 familyId 强制过滤而不是相信前端传的参数。Component public class UserContext { public static Long getFamilyId() { // 从 ThreadLocal 中取当前登录用户上下文 return ((LoginUser) SecurityHolder.get()).getFamilyId(); } }在查询报销单列表时Override public IPageClaimVO pageClaim(ClaimQuery query) { Long currentFamilyId UserContext.getFamilyId(); LambdaQueryWrapperClaim wrapper new LambdaQueryWrapper(); // 关键强制加上 familyId 条件防止水平越权 if (currentFamilyId ! null) { wrapper.eq(Claim::getFamilyId, currentFamilyId); } if (StrUtil.isNotBlank(query.getStatus())) { wrapper.eq(Claim::getStatus, query.getStatus()); } // 这里是完整的业务查询逻辑 return claimMapper.selectPage(new Page(query.getPageNum(), query.getPageSize()), wrapper); }这里的管理员属于特殊角色familyId 为 null此时不加家庭条件可以查看全部数据。这一套就是面试题里常问的“行级权限/数据权限”的实现思路毕设里写出来很加分。4.2 报销核算引擎别再用 double 算钱报销核算的核心方法是把就诊费用、报销比例、剩余额度三个变量组装成最终报销金额。我建议单独建一个 ReimburseCalculator 组件不要写在 Controller 里。Component public class ReimburseCalculator { /** * 计算单条就诊记录的报销金额 * * param fee 就诊总费用 * param rate 报销比例0.75 表示报销 75% * param remainLimit 剩余可用额度 * return 实际报销金额 */ public BigDecimal calc(BigDecimal fee, BigDecimal rate, BigDecimal remainLimit) { BigDecimal expect fee.multiply(rate) .setScale(2, RoundingMode.HALF_UP); if (expect.compareTo(remainLimit) 0) { // 剩余额度不够时最多报剩余额度那么多 return remainLimit; } return expect; } }为什么用 BigDecimal 而不用 double0.1 0.2 在二进制浮点里等于 0.30000000000000004报销账目一旦出现这种误差审核人员核对发票时就会发现问题。BigDecimal 的金额计算一定要用字符串构造入参new BigDecimal(0.75) 而不是 new BigDecimal(0.75)后者同样会有精度问题。核算后还要做一步“超额检查”。例如保险方案年度额度 3 万元家庭已经用了 2.8 万现在新申请核算出 5000 元实际只能报 2000 元剩余额度清零。具体的计算在事务里执行Transactional(rollbackFor Exception.class) public ClaimVO submitClaim(ClaimSubmitDTO dto) { // 1. 校验参保状态 // 2. 锁定方案额度防止并发超报 InsurancePlan plan planMapper.selectByIdForUpdate(dto.getPlanId()); // 3. 逐条核算 for (MedicalRecordDTO record : dto.getRecords()) { BigDecimal remain plan.getAnnualLimit().subtract(plan.getUsedAmount()); BigDecimal reimburse calculator.calc(record.getFee(), plan.getRate(), remain); // 4. 更新已用额度 plan.setUsedAmount(plan.getUsedAmount().add(reimburse)); } // 5. 生成报销单和明细 return claimVO; }这里藏着几个容易踩的坑selectByIdForUpdate 是行级锁防止两个报销单同时提交导致额度超用事务要加在“校验扣减生成”的整个方法上否则额度扣了但单子没生成数据就错了Transactional只对抛 RuntimeException 回滚如果手动捕获异常不抛出事务不会回滚这点很多新手容易翻车。4.3 额度预警与通知提醒预警不必做成复杂定时任务至少有三个可行做法查询时计算家庭看板接口返回时动态计算 annualLimit、usedAmount、remainPercent如果 remainPercent 20返回一个 warning 字段。前端看到 warning 就显示提示条。提交报销后判断报销单审核通过时系统检查该家庭剩余额度如果低于 20%自动生成一条系统消息。定时任务兜底使用 Spring 的 Scheduled 每天凌晨扫描一次接近额度的家庭给户主账号生成站内信。我建议至少实现前两种演示效果立竿见影。第三种的 Scheduled 可以写在论文里作为扩展点不需要真的跑起来。消息提醒如果要用 WebSocket 实时推送配置也不复杂核心就是后端握手接口加一个 /topic 订阅通道前端用 stomp 客户端接收。这属于加分项时间不够就先用轮询。4.4 文件上传发票与 PDF 导出发票文件上传的重点是限制大小和类型。我见过有人没配大小限制一次上传 200MB 的扫描件直接把 Tomcat 默认的 1MB 限制顶炸了报错还非常不直观。配置里应该这样spring: servlet: multipart: max-file-size: 5MB max-request-size: 10MB上传成功后把文件 URL 存到 medical_record 的 invoice_url 字段。这里要注意本地磁盘存文件时URL 里不要把磁盘绝对路径返回给前端而是通过一个资源映射统一对外Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String uploadPath System.getProperty(user.dir) /upload/; registry.addResourceHandler(/files/**) .addResourceLocations(file: uploadPath); } }PDF 导出我建议用这种方式后端把审核通过的报销单数据渲染成 HTML 模板再用 openhtmltopdf 或 itextpdf 转成 PDF 输出。这个方案比手写 PDF 坐标要快得多而且样式可控。导出后的报销单支持在线预览和下载正好补全“家庭医疗保险服务系统”的服务闭环。如果你时间紧最简单的是前端直接调 window.print() 打印页面但 PDF 导出这个功能在答辩时是加分项值得做。5. 前端页面设计与联调细节5.1 页面模块与路由设计前端如果选 Vue3 Element Plus可以按业务角色把页面分成三块家庭成员端家庭看板额度进度、近期报销记录、成员档案、参保方案、就诊记录登记、报销申请提交、消息通知、报销单查看下载。审核管理端待审报销单列表支持按家庭、时间、状态筛选、报销单详情核对、通过/驳回操作、审核记录查询。系统管理端保险方案维护、医院等级和费用类型配置、家庭成员账号管理、系统公告发布、数据统计报表。路由结构可以这样设计/family/dashboard 家庭看板 /family/member 成员档案 /family/claim/new 提交报销 /family/claim/list 我的报销单 /audit/list 审核任务列表 /audit/detail/:id 审核详情 /admin/plan 保险方案维护 /admin/statistics 统计报表页面间的跳转逻辑要交代清楚。比如报销申请流程的页面顺序是选择成员 → 选择就诊记录支持多选→ 系统自动核算 → 确认提交。每一步对应后端一个接口联调的时候按这个顺序一条条过不容易漏。5.2 前后端联调三大坑CORS、Token、时间格式这三个问题几乎每个做前后端分离的同学都会遇到提前知道能省好几天。CORS 跨域开发环境 Vue 跑在 5173 端口后端跑在 8080 端口浏览器会拦截跨域请求。后端加一个配置类实现 WebMvcConfigurer 的 addCorsMappings允许来源 http://localhost:5173允许所有请求头和方法。更稳妥的做法是在前端 Vite 里配代理把 /api 开头的请求转发到 8080这样浏览器看到的是同源请求完全不触发跨域。Token 携带前端用 axios 拦截器统一在请求头里塞 Tokenaxios.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer token; } return config; });后端拦截器校验失败时统一返回 401前端 axios 响应拦截器检测到 401 就清掉本地 Token 并跳转登录页。这里关键是处理逻辑要放在拦截器里而不是每个页面重复写。时间格式LocalDateTime 序列化后默认是数组或带 T 的 ISO 格式前端展示会很难看。后端全局配置 Jackson 序列化格式把 LocalDateTime 统一输出为 yyyy-MM-dd HH:mm:ssLocalDate 输出为 yyyy-MM-dd。这样前端表格、详情页拿到就是可直接展示的字符串不需要再拿 dayjs 去格式化。6. 常见问题与踩坑避雷指南6.1 必踩的坑与排查清单我按遇到频率从高到低整理了一张速查表这些都是真实项目调试里被反复验证过的问题不是网上抄的现象直接原因处理办法启动后访问接口报 404Controller 包路径不在启动类扫描范围内启动类放在 controller/service/mapper 包的最外层数据库中文乱码连接串没指定 UTF-8URL 加 characterEncodingutf8useSSLfalse上传文件报错 500Tomcat 默认限流太小配置 spring.servlet.multipart.max-file-size时间显示为 “2025-04-01T10:20:30”LocalDateTime 默认序列化格式全局配置 Jackson 日期格式报销金额显示 0.30000000000004使用 double 计算金额全链路改用 BigDecimal明明登录了还跳登录页Token 过期时间设太短JWT 过期时间设 24 小时或者做续期审核通过后额度没变事务没加或者异常被吞检查 Transactional 和 catch 块是否重新抛出 RuntimeException列表接口数据超出当前家庭范围没有做行级数据过滤Service 层强制拼接 family_id 条件6.2 从“能跑”到“好讲”的优化点如果你的时间有富余我建议把下面三个点做了这些在答辩时非常能体现工程意识接口文档接入 Knife4j 后自动生成 Swagger 文档Controller 注解一标答辩时打开文档页面展示接口比复制代码有说服力得多。日志与审计用一个 AOP 切面统一记录操作日志记录谁在什么时间操作了哪条数据。医保系统本身对审计有要求这个东西写在论文里是很自然的业务需求。健康检查与启动 Banner项目启动时打印一个自定义 banner试试改掉默认的 SpringBoot 图案这个虽然在功能上没意义但在演示时观赏性好也算一种细节感。还有一个工程配置细节application.yml 里的数据源密码不要明文写在配置里。可以写成环境变量引用${DB_PASSWORD}本地开发时用 IDEA 的环境变量或启动参数传入。虽然毕设不做生产部署但这个习惯在面试时能聊两句比如分布式配置中心、敏感信息脱敏都是后端领域的高频面试话题。7. 答辩经验与个人心得最后聊一点答辩时很实用的经验。这个项目最容易打动人、也最容易被追着问的地方其实就三个数据权限怎么做、报销并发怎么防、智能规则怎么配置。我在带这块项目时见过导师最爱问“如果一个家庭同时提交两笔报销剩余额度只有 5000 元你系统怎么保证不会超额”——这就是第 4.2 节的行级锁和事务问题答得出来你就是真正理解了这个系统答不出来就会显得项目是抄的。建议把这段代码的调用链路背下来Controller 接收请求 → Service 开启事务 → selectByIdForUpdate 锁行 → 核算剩余额度 → 更新已用额度 → 提交事务。还有一个容易忽略的点演示前一定准备好一份“假数据脚本”。准确说是把家庭成员、医保方案、就诊记录、待审核报销单提前插入数据库。我见过太多演示现场现敲数据又慢又容易出 bug。提前把数据造好演示时顺着看板页面的额度进度条点进去一步一步展示报销的完整流程这种演示节奏是最舒服的。我自己做这类管理系统的整体感觉是它比图书管理、学生管理等老题材更有业务厚度又不至于复杂到无法驾驭。家庭医保的业务规则足够撑起一篇有逻辑的论文数据结构也足以支撑清晰的图表设计。只要把报销核算这条主线做稳、把额度预警和文件上传这两个亮点做出来再做点页面美化整体完成度就相当能打了。后续想扩展的话异步任务、对象存储、缓存预热、消息推送这些点都可以继续往里面加而且每个扩展点都能对应一个成熟的开源组件随手就能写出“本项目基于 xxx 实现了 xxx”的加分句。真到答辩前把这套东西从头到尾走两遍你心里就有底了。