news 2026/10/9 6:19:02

SpringBoot智能家庭医保管理系统开发全流程:从建模到权限与状态机

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot智能家庭医保管理系统开发全流程:从建模到权限与状态机

每年三四月份,总有一批毕业生陷入同一种纠结:题目定了,但题目给的只是一句话,剩下的全靠自己脑补。“Java 智能家庭医疗保险管理系统,SpringBoot 做 Web 版家庭医保管理平台”就是这么一类典型题目——看着很有分量,实际上一开始根本不知道从哪落笔。我带学弟完整做过一遍这个项目,这篇就当作复盘记录,把从需求翻译、技术选型、数据库建模到核心接口实现、答辩前自检的完整路径讲清楚。适合三类人看:正在为这个题目掉头发的高校学生、想用 SpringBoot 快速搭一个管理系统类项目的 Java 开发者,以及手上有“家庭 + 医疗/保险/社区服务”这类业务场景、需要找一套最小可用系统设计作参考的人。

1. 先从“家庭”这个角度拆透业务场景

1.1 医保系统在管什么:三条业务主线

“家庭医疗保险”这个题目的特殊性,落在“家庭”两个字上。它不是一个人在买保险,而是一个家庭账户下挂着多个成员——老人、配偶、孩子,各自有参保身份和就医报销需求。所以业务上天然分成三条流:

  • 参保流:用户注册后创建家庭或加入一个家庭,维护家庭成员档案,选择参保套餐,生成参保订单。
  • 缴费流:订单产生后完成缴费,系统记录每一笔缴费的来源、金额、方式,并在到期前做续费提醒。
  • 报销流:家庭成员就医后,以家庭维度提交报销申请,审核人员受理、按规则核算可报金额,经终审通过后进入打款状态。

把这三条流画在同一张图上,会发现数据流转方向非常清晰:家庭是根,用户挂在家庭下;参保订单和缴费记录挂在家庭下;报销申请挂在具体的家庭成员下,最终统计的时候又可以按家庭聚合。这种“一个根节点下挂多个子节点”的结构,在一开始建表时就想好,后面写接口会省非常多事。

1.2 “智能”两个字在答辩现场怎么讲才不虚

很多学生被题目里的“智能”劝退,觉得自己不会 AI,不敢选这个题。其实对于本科毕设来说,“智能”完全可以是业务规则层面的智能化,不是必须上算法。我在做这套系统时,把“智能”具体落到了四个功能上:

  • 阶梯式报销比例计算:不同医院等级、不同费用段按不同比例核算,而不是拍脑袋填一个数。
  • 家庭年度报销额度预警:实时统计当前家庭当年已报销金额,达到设定阈值时给用户推送消息。
  • 参保到期自动提醒:扫描订单的到期时间,提前 7 天生成通知。
  • 多维度统计报表:按月、按病种、按费用段聚合报销数据,给管理员做辅助决策。

这四个功能全部是规则加统计,用定时任务、SQL 聚合、简单 if-else 逻辑就能完成,但对答辩来说,它就是“智能”的实锤。评委问“智能在哪”的时候,你至少有四个能演示、能讲原理的落点,而不是空谈系统名称。

1.3 从一句话题目到完整功能清单

拿到题目的第一个动作,是把一句话翻译成功能模块。以这个题目为例,我会拆成三端一公共:

端功能模块包含功能面向角色
用户端家庭管理创建家庭、添加/移除成员、编辑成员健康档案普通用户
用户端参保缴费查看套餐、购买套餐、创建订单、模拟支付、缴费记录查询普通用户
用户端报销管理提交报销申请、上传票据图片、查看审核进度、撤销未受理申请普通用户
审核端报销审核受理申请、录入核算信息、自动计算报销金额、通过/驳回审核员
管理端系统管理用户账号管理、角色分配、套餐维护、医院等级维护管理员
管理端统计报表按月份/病种/费用段统计报销数据,导出 Excel管理员
公共通知中心审核结果通知、缴费到期提醒、年度额度预警全部角色

这样一个表格出来,开发量就清楚了。绝大多数模块都是标准增删改查,真正的业务难点集中在“报销审核的状态流转”和“报销金额计算规则”上,后面专门展开。

2. 技术选型:为什么是 Java + SpringBoot,而不是更花哨的方案

2.1 单体架构是故意选的,不是不懂微服务

写这类系统时很容易被网上“分布式、微服务、高并发”的氛围带偏,毕设里硬塞 Nacos、Sentinel、网关这类组件。我的建议非常直接:不要。原因有三个。第一,本科毕设评审更看重你是否理解每一行自己写的代码,微服务体系下你大概率只复制了配置,讲不清楚核心逻辑。第二,单体架构加分层设计(Controller → Service → Mapper)足够你展示设计能力,面试官问“为什么这么分层”你也能答得透彻。第三,部署简单,一个 SpringBoot 内置 Tomcat 的 jar 包就能跑,答辩现场不会因为服务互相起不来而翻车。

家庭医保平台的用户规模、数据量、事务复杂度都远没到必须拆分的程度,它需要的是清晰的数据边界和可靠的状态流转,而这些恰好是单体架构最拿手的。

2.2 一套实测最稳的版本组合

“SpringBoot 版本太高”是我在相关搜索里见到的高频问题,也是本项目中新手最容易踩的坑。我最终采用的组合如下:

组件推荐版本为什么选它
JDK1.8 或 17教室和老电脑最稳的是 1.8;如用 SpringBoot 3.x 才需要 17
SpringBoot2.7.18教程多、资料全、用的是 javax 包;3.x 改成 jakarta 后老代码直接报错
MyBatis-Plus3.5.3 及以上代码生成、分页插件、逻辑删除、字段自动填充,省掉大量重复工作
MySQL8.0主流版本;5.7 也可以,DDL 差异不大
前端Thymeleaf 或 Vue3不会前端用 Thymeleaf,服务端渲染容易讲;熟悉前端用 Vue3 + Element Plus
构建工具Maven比 Gradle 普及,遇到问题搜解决方案容易

这里重点说一下为什么不是最新版 SpringBoot 3.x。3.x 把 Java 持久层 API 的包名从 javax.* 换成了 jakarta.,很多早期教程里的 import javax.servlet.全部失效,连启动都会报错。对于毕设来说,时间比版本新更重要,选用 2.7.18 可以让你在网上找到 90% 以上的老教程做参考,少踩一半的坑。如果老师坚持要新版本,那就老老实实配 17 以上 JDK,并且全程用 jakarta 前缀,不要混用教程。

2.3 一次“提交报销单”请求到底经历了什么

技术栈选定后,我习惯用一次具体请求来检验自己是否真正理解这套架构。以用户提交报销单为例:

浏览器里用户填好表单,点击提交;前端把数据封装成 JSON 发到后端 /api/reimburse/submit;SpringBoot 先经过拦截器校验用户是否登录、角色是否合法;然后进入 ReimburseController,Controller 不写业务逻辑,只把参数交给 ReimburseService;Service 做业务校验——判断家庭成员是否属于当前用户家庭、票据是否完整、数据库里写入一条状态为“已提交”的报销申请;Mapper 负责把数据持久化到 MySQL,并返回自增主键;最后 Controller 把结果包装成统一 Result,返回给前端。

对应到代码上,三层结构的骨架长这样:

// Controller 层:只做请求接收和结果返回 @RestController @RequestMapping("/api/reimburse") public class ReimburseController { private final ReimburseService reimburseService; @PostMapping("/submit") public Result<Long> submit(@RequestBody ReimburseSubmitDTO dto) { return Result.success(reimburseService.submit(dto, CurrentUser.get().getId())); } } // Service 层:写业务规则 public interface ReimburseService extends IService<ReimburseApply> { Long submit(ReimburseSubmitDTO dto, Long userId); } // Mapper 层:负责 SQL @Mapper public interface ReimburseApplyMapper extends BaseMapper<ReimburseApply> { List<ReimburseStatVO> statByMonth(@Param("familyId") Long familyId); }

这个分层的好处是:Controller 里看不到业务,Service 里看不到 SQL,Mapper 里只写持久化。答辩时被问到任何一层,都能准确说出它的职责边界。

3. 数据库建模:这套系统的表到底怎么设计才经得起答辩

3.1 家庭表和用户表:数据隔离的根基

数据库设计是整个毕设的“门面”,评委往往第一眼看 ER 图。家庭医保系统的核心关系就是“家庭-成员”的一对多,以及“成员-报销单”的一对多。我给出的家庭表设计如下:

CREATE TABLE `family` ( `id` bigint NOT NULL AUTO_INCREMENT, `family_name` varchar(50) NOT NULL COMMENT '家庭名称', `head_user_id` bigint DEFAULT NULL COMMENT '户主用户ID', `contact_phone` varchar(20) DEFAULT NULL COMMENT '联系电话', `address` varchar(200) DEFAULT NULL COMMENT '家庭地址', `status` tinyint DEFAULT 1 COMMENT '状态:1正常 0停用', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, `deleted` tinyint DEFAULT 0 COMMENT '逻辑删除', PRIMARY KEY (`id`) ) ENGINE = InnoDB COMMENT = '家庭信息表';

用户表要同时承担“家庭成员”和“平台角色”两个身份:

CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录账号', `password` varchar(100) NOT NULL COMMENT 'BCrypt加密密码', `real_name` varchar(30) DEFAULT NULL COMMENT '真实姓名', `role` varchar(20) NOT NULL COMMENT '角色:ADMIN/AUDITOR/USER', `family_id` bigint DEFAULT NULL COMMENT '所属家庭ID,管理员可为空', `gender` tinyint DEFAULT NULL COMMENT '性别', `birth_date` date DEFAULT NULL COMMENT '出生日期', `id_card` varchar(18) DEFAULT NULL COMMENT '身份证号', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `status` tinyint DEFAULT 1 COMMENT '账号状态', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, `deleted` tinyint DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`), KEY `idx_family_id` (`family_id`) ) ENGINE = InnoDB COMMENT = '系统用户表';

这里有两个设计细节值得注意。第一,role 和 family_id 之间有关系:USER 必须有 family_id,ADMIN 和 AUDITOR 的 family_id 为空。这样在做数据隔离时,普通用户的查询天然会带上 family_id,而审核员和管理员走另一套管理查询逻辑,不会混在一起。第二,我没有建物理外键,只用逻辑关联。物理外键在插入更新时有性能开销,删除时也容易引发连锁约束问题,毕设阶段用代码保证业务一致性足够了。

3.2 参保、缴费、报销三张业务表

业务核心的三张表,我分别给出关键字段。

参保订单表。每次购买套餐生成一条订单,订单号是业务追踪的关键标识:

CREATE TABLE `insurance_order` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单号:年月日+序号', `family_id` bigint NOT NULL COMMENT '购买家庭', `package_id` bigint NOT NULL COMMENT '套餐ID', `start_date` date NOT NULL COMMENT '生效日期', `end_date` date NOT NULL COMMENT '到期日期', `amount` decimal(10,2) NOT NULL COMMENT '订单金额', `status` tinyint DEFAULT 0 COMMENT '0待支付 1已支付 2已取消', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE = InnoDB COMMENT = '参保订单表';

缴费记录表。设计上它可以是订单的附属表,但单独建更利于统计“钱从哪里来”:

CREATE TABLE `payment_record` ( `id` bigint NOT NULL AUTO_INCREMENT, `payment_no` varchar(32) NOT NULL COMMENT '支付流水号', `order_id` bigint NOT NULL COMMENT '关联订单', `paid_by` bigint NOT NULL COMMENT '支付人用户ID', `amount` decimal(10,2) NOT NULL, `pay_method` varchar(20) DEFAULT NULL COMMENT '支付方式/平台', `pay_time` datetime DEFAULT NULL COMMENT '支付时间', `status` tinyint DEFAULT 1 COMMENT '1成功 0失败', PRIMARY KEY (`id`), UNIQUE KEY `uk_payment_no` (`payment_no`) ) ENGINE = InnoDB COMMENT = '缴费记录表';

报销申请表是整个系统最重要的一张表。它承载了从用户提交到审核员操作的全过程,状态字段是所有业务流程的开关:

CREATE TABLE `reimburse_apply` ( `id` bigint NOT NULL AUTO_INCREMENT, `req_no` varchar(32) NOT NULL COMMENT '报销单号', `family_id` bigint NOT NULL COMMENT '所属家庭', `member_id` bigint NOT NULL COMMENT '就医成员ID(用户ID)', `member_name` varchar(30) NOT NULL COMMENT '冗余成员姓名,避免多次关联查询', `hospital_name` varchar(100) DEFAULT NULL COMMENT '就诊医院', `hospital_level` varchar(20) DEFAULT NULL COMMENT '医院等级:三甲/三乙/二甲/社区', `visit_date` date DEFAULT NULL COMMENT '就诊日期', `disease_type` varchar(50) DEFAULT NULL COMMENT '病种/科别', `total_amount` decimal(12,2) NOT NULL COMMENT '医疗总费用', `apply_amount` decimal(12,2) DEFAULT NULL COMMENT '申请报销金额', `computed_amount` decimal(12,2) DEFAULT NULL COMMENT '系统核算金额', `audit_comment` varchar(500) DEFAULT NULL COMMENT '审核意见', `status` tinyint DEFAULT 0 COMMENT '0已提交 1已受理 2已核算 3已通过 4已打款 -1已驳回', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_family_status` (`family_id`, `status`) ) ENGINE = InnoDB COMMENT = '报销申请表';

报销表里做了两个设计上的小冗余:member_name 冗余了姓名,hospital_name 冗余了医院名。因为查询报销列表时要经常展示这两个字段,与其每次 join 用户表、医院表,不如在提交时直接写一份,省联查也方便导出。

3.3 统一字段和接口共性:MyBatis-Plus 帮你省下的工作量

三张业务表都有 create_time 和 update_time,如果每个新增方法里手动 set 一次,代码会非常冗余。MyBatis-Plus 提供了字段自动填充,在实体上只需要声明:

@Data public class ReimburseApply { @TableId(type = IdType.AUTO) private Long id; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }

然后配置一个 MetaObjectHandler:

@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }

逻辑删除也同理,在实体字段上加 @TableLogic,SQL 查询时 MyBatis-Plus 会自动追加 deleted = 0 条件。代码生成器可以从现有表反向生成实体、Mapper、Service、Controller,然后在生成结果上改业务逻辑,而不是从零手写所有类。我建议用这种方式起步,把时间花在业务规则上,不要浪费在搭空壳。

4. 后端核心链路:登录鉴权、报销状态机与“智能”逻辑

4.1 登录鉴权与角色边界:拦截器加 ThreadLocal 就够了

三端功能都基于角色,所以鉴权必须放在请求入口统一处理。我不建议在毕设阶段引入完整的 Spring Security 全家桶,配置复杂,答辩时还容易被追问到不熟悉的细节。更可控的方案是:登录成功后生成一个 UUID 作为 token,存到 Redis(或内存 Map,演示够用),前端每次请求在 Header 里带上 token;后端写一个拦截器统一校验,并把当前登录用户信息放进 ThreadLocal,方便后续业务取用。

拦截器的核心逻辑:

public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); // 校验 token 是否有效 LoginUser user = tokenManager.getUser(token); if (user == null) { throw new BizException(401, "未登录或登录已过期"); } // 放入 ThreadLocal,供 Controller/Service 取用 CurrentUser.set(user); return true; } @Override public void afterCompletion(...) { CurrentUser.clear(); // 防止线程复用导致数据串线 } }

角色边界我建议控制到方法级别。定义一个 @RequireRole 注解,标注在 Controller 方法上,拦截器里读取注解并比对角色,不匹配直接返回 403。这样写出来的权限代码可读性很高,答辩时指着注解说“这一行就是权限声明”,比在 if 嵌套里找权限逻辑有说服力得多。

更重要的是数据边界。查询报销单时,不能信任前端传过来的 familyId,而是从当前登录用户身份里取:

// 错误示范:前端传啥查啥 List<ReimburseApply> list = mapper.selectByFamilyId(dto.getFamilyId()); // 正确示范:强制以当前登录用户的家庭为准 Long familyId = CurrentUser.get().getFamilyId(); List<ReimburseApply> list = mapper.selectByFamilyId(familyId);

这个习惯养成了,越权漏洞基本就堵死了。

4.2 报销审核状态机:让每一步操作都有据可查

报销审核是整套系统的业务核心,它本质是一个状态流转过程。如果只用 update 语句随便改状态,会出现“审核员跳过受理直接打款”这类荒谬情况。所以我在 Service 里维护了一张状态流转表,每次操作前先校验当前状态是否允许该操作:

当前状态可执行操作目标状态
0 已提交受理 / 驳回1 已受理 / -1 已驳回
1 已受理核算2 已核算
2 已核算通过 / 驳回3 已通过 / -1 已驳回
3 已通过打款4 已打款

对应到代码,用枚举维护状态和合法转移比散落一堆 if 判断更可靠:

public enum ReimburseStatus { SUBMITTED(0, "已提交"), ACCEPTED(1, "已受理"), COMPUTED(2, "已核算"), APPROVED(3, "已通过"), PAID(4, "已打款"), REJECTED(-1, "已驳回"); public static boolean canGo(Integer current, ReimburseStatus target) { return switch (current) { case 0 -> target == ACCEPTED || target == REJECTED; case 1 -> target == COMPUTED; case 2 -> target == APPROVED || target == REJECTED; case 3 -> target == PAID; default -> false; }; } }

每次状态变更同时写一条审核日志,记录操作人、操作时间、原状态、新状态、备注。这样即使被驳回,用户也能在“审核进度”里看到完整历史,而不是只看到一个孤零零的“已驳回”。这条链路完整,答辩时拿出来讲,评委能立刻感受到流程设计的严谨性。

4.3 报销金额计算:把“智能”做成可解释的规则

“智能”中最好演示、也最容易引发追问的就是报销金额计算。我建议把它做成一个独立方法,规则明确、可单测,而不是在东一段西一段的代码里拼。一个能站住脚的规则是“医院等级比例加家庭年度封顶”的组合:

  • 按医院等级定基础比例:社区医院 90%,二级医院 80%,三级医院 70%。
  • 按费用段做阶梯校正:单次费用超过 1 万的部分比例下调 10%,防止大额报销失控。
  • 按家庭年度累计封顶:单个家庭每年报销总额上限 5 万(这个数值可按项目自由设定),超出部分不再报销。

核心方法长这样:

public BigDecimal computeReimburseAmount(BigDecimal total, String hospitalLevel, BigDecimal familyYearUsed) { // 1. 基础比例 BigDecimal ratio = switch (hospitalLevel) { case "社区医院" -> new BigDecimal("0.90"); case "二级医院" -> new BigDecimal("0.80"); case "三级医院" -> new BigDecimal("0.70"); default -> new BigDecimal("0.60"); }; // 2. 阶梯校正:超过1万部分比例下调10% BigDecimal threshold = new BigDecimal("10000"); if (total.compareTo(threshold) > 0) { BigDecimal over = total.subtract(threshold); BigDecimal baseCalc = threshold.multiply(ratio); BigDecimal overCalc = over.multiply(ratio.subtract(new BigDecimal("0.10"))); BigDecimal amount = baseCalc.add(overCalc); // 3. 年度剩余额度封顶 BigDecimal remain = FAMILY_CAP.subtract(familyYearUsed); return amount.min(remain); } BigDecimal amount = total.multiply(ratio); BigDecimal remain = FAMILY_CAP.subtract(familyYearUsed); return amount.setScale(2, RoundingMode.HALF_UP); }

这段代码的价值在于:每一行都能回答“为什么”。比例为什么不同?因为医保分级诊疗导向。为什么封顶?因为保险的基本原则是共担风险,不是无限报销。而这些,恰恰是评委最爱问的问题。计算发生后,记得把 computed_amount 和计算依据(比例、封顶情况)写入审核日志,前端审核页展示“系统建议报销金额”,审核员可以确认或修改——既体现智能辅助,又保留人工兜底。

4.4 定时任务与消息提醒:让系统“主动”起来

“智能家庭医保”还有一个好实现但容易被忽略的点:定时提醒。比如用户参保快到期了、年度报销额度用掉 80% 了,系统要主动通知,而不是等用户自己发现。这一步用 Spring 的 @Scheduled 就能做:

@Component public class RemindTask { @Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3点 public void insuranceExpireRemind() { // 查询7天内到期的参保订单 List<InsuranceOrder> orders = orderMapper.selectExpireSoon(LocalDate.now(), 7); for (InsuranceOrder order : orders) { notificationService.send( order.getFamilyId(), "参保即将到期", "您的家庭参保套餐将于 " + order.getEndDate() + " 到期,请及时续费" ); } } }

提醒统一落到 notification 表,用户端在首页展示未读数量和列表。这类功能不用复杂算法,却能直观体现“智能”二字,也顺手把定时任务、消息模块、首页展示串成了一个完整功能群。

5. 隐藏的工作量:分页、搜索、导出这些功能别低估

5.1 分页:MyBatis-Plus 一个拦截器的事

管理端列表页面基本都要分页。MyBatis-Plus 的分页插件配置很简单:

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

查询时直接用 Page 对象:

IPage<ReimburseApply> page = mapper.selectPage( new Page<>(query.getPage(), query.getSize()), queryWrapper );

关于分页有个容易踩的点:不要在代码里自己用 LIMIT 拼接偏移量,一是不安全,二是不同数据库方言不兼容。MyBatis-Plus 分页插件内部帮我们做了方言适配,换数据库也不用改业务代码。

5.2 多条件搜索:把动态条件从代码里“长”出来

报销列表经常要按状态、时间、医院名筛选。如果用字符串拼接 SQL,极易产生注入风险。正确做法是用 LambdaQueryWrapper 动态构建条件:

public IPage<ReimburseApply> pageQuery(ReimburseQuery query) { LambdaQueryWrapper<ReimburseApply> wrapper = Wrappers.lambdaQuery(); wrapper.eq(query.getFamilyId() != null, ReimburseApply::getFamilyId, query.getFamilyId()); wrapper.eq(query.getStatus() != null, ReimburseApply::getStatus, query.getStatus()); wrapper.between(query.getStartDate() != null && query.getEndDate() != null, ReimburseApply::getCreateTime, query.getStartDate(), query.getEndDate()); wrapper.like(StringUtils.hasText(query.getHospitalName()), ReimburseApply::getHospitalName, query.getHospitalName()); wrapper.orderByDesc(ReimburseApply::getCreateTime); return mapper.selectPage(new Page<>(query.getPage(), query.getSize()), wrapper); }

这套代码的核心是“条件对象 ReimburseQuery 加 Lambda 表达式”,条件为空时对应方法不生效,动态拼接完全由框架处理。前端筛选项再多,后端也只是在 wrapper 上多挂一个条件,不会把查询方法写成四五个重载。

5.3 Excel 导出:答辩加分项,EasyExcel 十分钟搞定

统计报表如果只能看不能导,总感觉差了点什么。引入 Alibaba EasyExcel 后,导出几乎不需要写底层 POI 代码:

@GetMapping("/export") public void export(HttpServletResponse response) throws IOException { List<ReimburseExcelVO> list = reimburseService.listForExport(); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); response.setHeader("Content-Disposition", "attachment;filename=reimburse.xlsx"); EasyExcel.write(response.getOutputStream(), ReimburseExcelVO.class) .sheet("报销记录") .doWrite(list); }

注意导出接口必须设置访问权限,只允许管理员调用。在实际操作中,我还遇到过中文文件名在部分浏览器里乱码的问题,稳妥的做法是文件名用 URLEncoder 编码后再放进 Content-Disposition。

6. 答辩前夜,我用一张清单把项目测了三遍

6.1 主链路必须完整跑通,每一步都要有数据留痕

答辩现场最怕的是演示时流程断在半路。我的经验是提前准备一套预置数据:至少三个登录账号(admin 管理员、auditor 审核员、user 家庭用户)、两个家庭、每户 2-3 个成员、几笔已完成的参保订单、几笔处于不同审核状态的报销单。

预置数据准备好后,按主链路完整走一遍:注册 → 登录 → 创建家庭 → 添加成员 → 购买套餐 → 模拟支付 → 提交报销 → 审核员受理 → 核算 → 通过 → 打款 → 用户端看到通知。每一步检查页面是否刷新出最新状态,数据库里是否有对应记录。这条链路一旦通畅,你的项目就已经具备了 80% 的完成度。

6.2 越权测试:这一步不过关,答辩会被当场问倒

很多项目功能齐全,却倒在权限漏洞上。我建议至少做这四组自测:

  • 普通用户 A 登录后,手动修改 API 参数里的家庭 ID,能否读到用户 B 的数据。如果能,说明后端没有强制从登录态获取 family_id。
  • 普通用户直接访问 /admin/xxx 路径,是否被拦截。如果没有,说明角色权限校验没生效。
  • 未登录状态下直接请求需要登录的接口,是否返回 401。如果返回业务数据,说明拦截器配漏了。
  • 审核员对状态为“已通过”的报销单再次点击“通过”,是否被拒绝。如果被拒绝,说明状态机生效了。

这四组测试最好以“能拦截”为通过标准。我在实际测试中,第一次往往能发现至少两三个漏洞,千万不要跳过去。

6.3 打包部署与交付材料

演示环境建议用 Maven 打包成 jar,稳定的操作路径是:

mvn clean package -DskipTests java -jar target/insurance-system.jar

SpringBoot 内置 Tomcat,不需要单独安装部署容器,一条命令就能把系统跑起来。首次启动前先执行项目里的 sql/init.sql 初始化数据库,再准备一个 README,写清楚 JDK 版本、MySQL 连接信息、初始账号密码,方便评委在自己电脑上复现。把这三个文件放进项目根目录,整个交付就完整了:数据库脚本、jar 包、说明文档。

今年带着学弟把这套系统完整做下来之后,我最大的体会是:这类“题目很响”的管理系统项目,真正拉开差距的不是技术多前沿,而是你有没有把业务边界理清楚、把状态流转做严谨、把越权漏洞堵干净。如果时间有限,优先保住报销主链路和权限控制这两条命脉——这两条跑通了,剩下的模块都只是增删改查的堆叠。做项目过程中最大的收获,其实是学会把一个模糊的题目翻译成清晰的设计,这个过程比最后那一份代码更能说明你理解了软件开发到底是怎么一回事。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 6:18:01

GitHub日榜怎么读?从榜单机制到开源项目落地选型的方法

2026年10月2日的GitHub日榜&#xff0c;我照例蹲点刷了一遍。说实话&#xff0c;这一天上去的项目不算惊艳&#xff0c;但恰恰是这种“平常日子”的榜单&#xff0c;最能看出门道。很多人把GitHub热榜当成“今日热门商品橱窗”&#xff0c;看一眼就走&#xff0c;这其实浪费了它…

作者头像 李华
网站建设 2026/10/9 6:17:55

文档管理中的权限控制机制:从RBAC到ABAC的选型与落地实践

做企业文档管理这几年&#xff0c;我见过太多团队在权限控制上栽跟头。有的公司内部资料库明明做了账号密码保护&#xff0c;结果核心设计方案照样被离职员工拷走&#xff1b;有的团队用共享网盘存合同&#xff0c;一个链接发出去&#xff0c;整个部门甚至外部合作方的账号都能…

作者头像 李华
网站建设 2026/10/9 6:17:36

两级式双向OBC仿真全解析:从三相PFC到CLLC的V2G/G2V建模与调试

1. 从G2V到V2G&#xff1a;两级式OBC在整车能量链里的位置1.1 OBC为什么成了新能源车的核心功率单元很多刚接触新能源仿真的工程师&#xff0c;一上来就直奔拓扑和波形&#xff0c;反而容易忽略一个最根本的问题&#xff1a;车载充电机&#xff08;OBC&#xff0c;On-Board Cha…

作者头像 李华
网站建设 2026/10/9 6:16:41

SolidWorks 2025安装指南:硬件配置、报错排查与Toolbox设置

每年到了新版本发布的时候&#xff0c;找我聊 SolidWorks 2025 安装的同事和朋友就特别多。有的是刚入行想跟上主流版本&#xff0c;有的是公司统一升级被 IT 部门要求先自己试装&#xff0c;还有的是从 2020、2021 一路用上来、想趁着换版本把电脑也理一理的老师傅。问来问去&…

作者头像 李华
网站建设 2026/10/9 6:16:06

AnyPS5:跨平台应用打包与分发的新思路与实践指南

1. 项目缘起与核心定位AnyPS5 这个名字第一次出现在我视野里的时候&#xff0c;我正被一堆跨平台构建脚本折腾得焦头烂额。简单来说&#xff0c;这是一个把“任意项目”快速打包成可分发、可运行、可复现的独立产物的工具链思路。它要解决的问题非常具体&#xff1a;你手头有一…

作者头像 李华
网站建设 2026/10/9 6:15:18

知网万方维普AIGC检测差异与跨平台降AI工具实操指南

上个月&#xff0c;一个硕士师弟拿着改了三遍的论文来找我&#xff0c;表情非常崩溃。他的综述在知网AIGC检测里已经降到了5%以下&#xff0c;结果投稿的期刊编辑部用的是万方系统&#xff0c;一测21.3%&#xff0c;直接退回修改。这种场景我这一年已经见了不下十次。很多人以为…

作者头像 李华