基于Java单体架构的养老管理系统:Spring Boot与MyBatis实践解析 简介中州养老系统设计源码采用Java开发定位为养老服务信息化平台围绕老年人日常生活与健康管理需求提供信息管理、服务预约、健康监测等能力适合Java开发者、毕业设计或养老类项目学习者参考。压缩包共115个文件其中98个Java源文件是业务实现主体覆盖界面交互、数据处理与核心逻辑13个XML配置用于数据库、网络通信等参数YML、JSON、gitignore、txt等辅助文件有助于理解项目配置方式与版本管理策略整包大小仅351KB。源码按common、web、service三大模块组织通用工具类、Web请求处理层和业务服务层分工明确可直观学习分层架构在真实养老场景中的落地。目前已有347人学习下载对于想要了解Java Web项目结构、老年人服务系统设计思路的读者来说是一份便于对照阅读和二次开发的实用源码。1. 中州养老系统一个Java单体项目为什么值得你动手复现先别把“养老系统”想成什么大平台它本质是一套面向养老机构养老院、护理院、社区日间照料中心的内部业务管理系统。老人从入院到退住的整个过程涉及建档、体检、护理计划、床位分配、费用缴纳、家属探望、护工排班这些数据如果还靠Excel和人肉传递一个一百张床的机构就能让管理员加班到崩溃。基于Java开发的中州养老系统设计源码做的就是把这一摊子事收进一个可部署的Web应用里前台给前台接待和护士站操作后台给院长和管理员看统计报表核心是“老人档案 照护任务 床位管理 费用结算”四件事。这个项目最值得复现的地方在于它不依赖大数据、不依赖微服务、不依赖分布式中间件就是一套标准的Java单体应用技术栈通常是Spring Boot MyBatis/MyBatis-Plus MySQL Vue或Thymeleaf正是目前Java岗位面试题里反复出现的那套组合。你把它跑通了等于把Java工程师招聘中最常问的CRUD、事务、权限、分页、报表全部亲手写了一遍。它适合谁正在做Java课程设计或毕业设计的学生刚入行想攒一个完整项目的初级开发以及小培训机构想给内部做一套管理系统——这三类人拿着这套源码都能在本地两小时内把它启动起来改改字段就能用。后面的内容我按自己做过类似管理系统的路子从业务建模、源码结构、表设计、踩坑到落地验证一层层拆给你看。这篇不会只贴代码会连“为什么这么设”一起讲。2. 从需求到模块先把养老系统的业务边界画清楚2.1 养老业务的四类核心角色与权限模型养老系统和普通电商系统最大的区别是它的数据有很强的“责任属性”。护工操作的是老人的生命体征和护理记录前台操作的是费用和床位院长看的是全院运营数据这三类人绝不能混在一个权限桶里。常见做法是把角色分成四类系统管理员管账号和基础数据、院办接待管入院登记、床位、费用、护士/护工管护理计划和执行记录、院长/管理层只看统计报表无写权限。权限落地用RBAC基于角色的访问控制就够了不需要引入Spring Security那一整套复杂过滤器链。我一般会建三张表sys_user账号、sys_role角色、sys_user_role关联表再上一步可以加sys_permission做菜单按钮级控制。对于这个体量的系统用户在登录后把角色编码放进Session或JWT里后端拦截器判断接口前缀就能控制访问。比如/api/care/**只允许nurse角色/api/report/**只允许manager角色拦截器里匹配一下角色编码字符串干净利落。要注意的点是“数据范围”不是所有护士都能看全院老人。常见做法是在护理记录表的查询条件里强制拼上当前登录护工的ID后端不能信任前端传参。这个细节如果漏了就会出现一个护工把自己的护理记录改到别人名下的数据权限事故。2.2 功能模块拆解档案、照护、床位、费用、统计把业务翻译成模块我习惯画一张“主数据 业务流转”的图老人主数据是系统的地基床位是资源护理计划是动作费用是结果。具体到页面和接口分成五个模块。老人档案模块负责入院登记、家属信息、健康档案既往病史、药物过敏、体检报告和退住记录。它和普通客户档案的区别在于老人档案里身份证号、联系电话是敏感字段列表页默认要脱敏显示详细页也要做查看权限控制。护理模块是核心中的核心包含护理计划按天或按班次生成、护理执行记录测血压、翻身、喂药、洗澡、异常情况上报。这个模块的难点是“计划要多级拆解”比如一个老人入住后医生定了一个长期照护方案护士每天要生成当天可执行的护理任务列表。床位模块管理房号、床号、床位类型单人间/双人间/多人间、当前状态空床、已入住、维修中。费用模块处理押金、月费、护理费、伙食费、临时加项费用月底要能生成账单支持预缴和欠费标记。统计模块是给院长看的比如总床位数、入住率、当月收入、护理任务完成率这些数据用定时任务每晚跑一次汇总到统计表比实时聚合查询快得多。2.3 技术选型为什么是Spring Boot MyBatis而不是其他如果你去搜过“java源码”相关的课程设计项目会发现90%都是Spring Boot MyBatis的组合这本身不是偶然。对这套系统来说Spring Boot负责简化配置和内嵌TomcatMyBatis负责把SQL写在XML里方便调整复杂的多表关联查询。为什么不选JPA养老系统的报表查询经常要跨老人表、床位表、费用表做统计JPA的自动装配在应对这类动态SQL时反而不如MyBatis直接写SQL直观。为什么不用Spring Cloud一个中等规模养老机构也就几十台电脑同时用单体应用扛得住没必要引入注册中心和分布式事务。MyBatis-Plus是这几年最常用的增强库它内置了单表CRUD方法代码生成器能直接根据数据库表生成实体、Mapper、Service、Controller四件套。对于这个项目我建议用MyBatis-Plus管理单表CRUD用自定义XML写关联统计SQL两者不冲突。表结构约定采用逻辑删除字段deleted和自动填充字段create_time、update_time这是MyBatis-Plus的常规配置后面几章会展开讲。技术选型里的“坑”大多是版本问题。Spring Boot 2.x配MyBatis-Plus 3.4.x很稳Spring Boot 3.x则需要MyBatis-Plus 3.5.3以上版本否则会因javax改成jakarta包名直接启动失败。如果你要复现这套源码先确认Java版本Spring Boot 2.7配Java 8或11别一上来就装Java 21踩版本兼容。3. 基于Java实现中州养老系统的项目结构源码包怎么组织3.1 标准三层结构controller/service/mapper/entity拿到一套养老系统源码第一件事不是看业务代码而是看包结构能不能“一眼定位”。我惯用的包结构如下com.zhongyang.oldcare ├── common // 通用类结果封装、异常处理、工具类 ├── config // 配置类MyBatis-Plus、CORS、拦截器 ├── controller // 控制器接收请求参数校验调用service ├── service // 业务层事务边界在这里处理业务逻辑 ├── mapper // 持久层继承BaseMapper或定义XML ├── entity // 实体类对应数据库表多用TableName注解 ├── dto // 数据传输对象VO用于展示DTO用于接收参数 └── interceptor // 登录与权限拦截器这个结构的好处是“哪里坏了看包名”。前端报错提示护理计划保存失败先看controller层有没有接住参数再看service层有没有加事务最后看mapper层的SQL有没有写错关联条件。新手最容易犯的错是把业务逻辑写到controller里导致一个保存接口塞了几百行代码出问题后Debug都要翻半天。正确做法是controller只做三件事接收参数、调用service、返回统一Result对象业务逻辑全部下沉到service。实体类的命名也要注意。老人表我通常命名为Elder而不是OldMan或OldPerson。Elder在Java语义里更规范也符合MyBatis-Plus默认驼峰转下划线的映射规则——elderName自动映射为数据库列elder_name。如果你已经把表建好了字段命名又不规范需要在实体字段上标注TableField(原列名)这一步会在5.2节里提醒你。3.2 用MyBatis-Plus生成基础CRUD的SQL与代码最小可跑通步骤最常见的做法是先用数据库工具把t_elder表建好然后在Java项目里引入MyBatis-Plus的代码生成器一次性生成实体、Mapper、Service、Controller。这里给一份最小可跑通的代码生成器配置// CodeGenerator.java public class CodeGenerator { public static void main(String[] args) { // 数据源配置这里的url要带参数useSSLfalse否则MySQL 8会警告 DataSourceConfig dataSourceConfig new DataSourceConfig.Builder( jdbc:mysql://localhost:3306/zhongyang_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai, root, 123456) .build(); // 表名过滤只生成核心表避免把关联表也生成一遍 StrategyConfig strategyConfig new StrategyConfig.Builder() .addInclude(t_elder, t_bed, t_care_plan, t_fee_record) .addTablePrefix(t_) // 去掉表名前缀自动映射实体名 .entityBuilder().enableLombok() // 用Lombok减少getter/setter .controllerBuilder().enableRestStyle() // 生成RestController .build(); PackageConfig packageConfig new PackageConfig.Builder() .parent(com.zhongyang.oldcare) .moduleName() .build(); AutoGenerator generator new AutoGenerator(dataSourceConfig); generator.strategy(strategyConfig); generator.packageInfo(packageConfig); generator.execute(); } }这段代码的逻辑指定数据库连接后告诉生成器只要生成t_elder等四张表并且把表前缀t_去掉——这样表t_elder会生成实体类Elder避免实体名变成TElder那么别扭。生成后ElderMapper接口会继承BaseMapperElder自带selectById、insert、deleteById等方法单表CRUD以后不用再写SQL。参数说明里最值得注意的两个点一是addTablePrefix(t_)如果数据库表没有统一前缀就不要配置二是enableRestStyle()它让生成的Controller方法名不是add、edit而是标准的POST /elder、PUT /elder/{id}对后来做前后端分离更友好。生成器也会自动把t_care_plan表名的下划线转成驼峰生成CarePlan实体这只对单数表名有效如果表名是t_care_plans复数生成的实体会是CarePlans后面手写代码时看着难受建议建表时统一用单数。3.3 配置数据源与分页插件让列表页不卡死的两个参数养老系统的老人列表、护理记录列表都是大列表千万不能把全表数据select *查出来再在内存里过滤。MyBatis-Plus自带分页插件但很多新手忘记配置导致Page对象返回的只有当前页数据total却是0。正确配置如下// MyBatisPlusConfig.java Configuration public class MyBatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 分页插件放在最后先执行其他内拦截器 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 防止全表更新与删除的插件误操作时兜底 interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); return interceptor; } }配置之后Service层查询写法就是标准的// ElderServiceImpl.java 中的分页查询 PageElder page new Page(current, size); // current页码从1开始size每页条数 LambdaQueryWrapperElder wrapper new LambdaQueryWrapper(); wrapper.like(Elder::getElderName, keyword) // 模糊搜索姓名 .eq(Elder::getStatus, NORMAL) // 只查正常在住状态的老人 .orderByDesc(Elder::getCreateTime); // 按建档时间倒序 PageElder result elderMapper.selectPage(page, wrapper);说明一下这里的参数current和size来自前端的pageNum和pageSizeRequestParam接收后直接传给Page。LambdaQueryWrapper的最大好处是类型安全字段名写错会在编译期报错而不是运行时才发现“列名不存在”。分页插件会对selectPage自动生成LIMIT ?语句同时执行一条SELECT COUNT(*)做总数统计。如果列表数据量大会把COUNT也拖慢常见优化是给查询条件上的elder_name加普通索引或者把“仅查未删除数据”的deleted0条件写进全局拦截器避免每一条查询都手写。4. 核心业务表设计与字段落地老人档案与护理计划怎么建模4.1 老人档案表关键字段与类型选择养老系统的地基表是t_elder它的字段设计决定后面所有模块能不能顺畅关联。我见过很多翻车案例都是把“联系人电话”设计成int结果手机号变成科学计数法或者把身份证号设计成varchar(18)但没考虑旧身份证15位的情况导致导入时报错。这里给一份核心建表SQL你可以直接抄CREATE TABLE t_elder ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, elder_no varchar(32) NOT NULL COMMENT 老人编号业务上可读的唯一编码, elder_name varchar(64) NOT NULL COMMENT 老人姓名, gender tinyint NOT NULL DEFAULT 2 COMMENT 性别0-男 1-女 2-未知, id_card varchar(18) NOT NULL COMMENT 身份证号加密存储后在展示层脱敏, birth_date date DEFAULT NULL COMMENT 出生日期从身份证解析, health_level varchar(16) DEFAULT SELF_CARE COMMENT 健康等级自理/半自理/全护理, contact_name varchar(64) DEFAULT NULL COMMENT 紧急联系人姓名, contact_phone varchar(20) DEFAULT NULL COMMENT 紧急联系人电话varchar不能存数字, bed_id bigint DEFAULT NULL COMMENT 当前床位ID关联t_bed, status varchar(16) NOT NULL DEFAULT CREATED COMMENT 状态CREATED-已建档CHECKED_IN-已入住CHECKED_OUT-已退住, remark varchar(500) DEFAULT NULL COMMENT 备注, deleted tinyint NOT NULL DEFAULT 0 COMMENT 逻辑删除0-未删 1-已删, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_elder_no (elder_no), KEY idx_bed_id (bed_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT老人档案表;字段类型选择有几个硬规矩。第一手机号、身份证号这类“看起来像数字但不会参与计算”的字段一律用varchar否则数字太长时MySQL会截断查询条件也要加引号。第二性别用tinyint加注释不用char(1)存“男/女”否则Java实体里还得维护字符串映射。第三金额字段不要用float或double后面5.4会专门说建表费用表时直接用decimal(10,2)。第四时间字段用datetimeJava实体里对应LocalDateTime配合MyBatis-Plus的MetaObjectHandler自动填充创建和更新时间代码里不需要手动set。health_level这个字段很关键它决定护理模块生成的计划模板。比如“全护理”老人默认每天要生成翻身、清洁、喂药三条任务而“自理”老人只需要查房提醒。把这些逻辑做成字典表还是枚举类我建议用varchar存编码再在Java里定义一个枚举常量类比每次查数据库字典表快也比散落的魔法字符串好维护。4.2 护理计划与执行记录一对多关系的处理护理计划是养老系统和普通进销存系统最大的差异点。一张t_care_plan记录“某个老人某天某个班次需要做哪些事”下面挂多条t_care_execution记录形成一对多关系。常见的错误设计是只建一张t_care_record表把计划内容和实际执行混在一起。更好的做法是拆开CREATE TABLE t_care_plan ( id bigint NOT NULL AUTO_INCREMENT, elder_id bigint NOT NULL COMMENT 老人ID, plan_date date NOT NULL COMMENT 计划日期, shift varchar(16) NOT NULL COMMENT 班次DAY-早班 NIGHT-晚班, item_code varchar(32) NOT NULL COMMENT 护理项编码BLOOD_PRESSURE, TURN_OVER, FEEDING, item_name varchar(64) NOT NULL COMMENT 护理项名称冗余存储避免关联字典, expect_time time DEFAULT NULL COMMENT 预计执行时间, executor_id bigint DEFAULT NULL COMMENT 指定护工ID为空表示班上任何护工均可, status varchar(16) NOT NULL DEFAULT PENDING COMMENT PENDING-待执行 DONE-已完成 SKIPPED-已跳过, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_elder_date (elder_id, plan_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT护理计划表;CREATE TABLE t_care_execution ( id bigint NOT NULL AUTO_INCREMENT, plan_id bigint NOT NULL COMMENT 关联护理计划ID, actual_time datetime DEFAULT NULL COMMENT 实际执行时间, executor_id bigint NOT NULL COMMENT 执行护工ID, result_desc varchar(500) DEFAULT NULL COMMENT 执行内容如血压读数、翻身情况, abnormal_flag tinyint NOT NULL DEFAULT 0 COMMENT 是否异常1-是 0-否, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_plan_id (plan_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT护理执行记录表;为什么要把计划和执行分离因为计划是“该做什么”执行是“实际做了什么”。护士早上到岗先查看当天的护理计划列表逐项执行后填写结果。如果老人中途发烧需要加测体温此时只在执行表里加一条记录不修改计划表这样月底统计出来“计划完成率”才有意义。代码里保存执行记录时要先SELECT plan_id FOR UPDATE锁住计划行再更新计划状态为DONE防止两个护工同时处理同一条计划造成脏数据。这里的“坑”在班次概念。养老院是24小时照护护工分白班和夜班如果计划表里没有shift字段第二天白班护工会看到昨天夜班的未完成任务直接乱套。生成计划时我习惯用定时任务在每天凌晨3点执行根据老人健康等级和care_template配置批量插入当天计划。这个定时任务要加Scheduled(cron 0 0 3 * * ?)并且用分布式锁兜底否则多实例部署时会重复生成计划。不过单体应用部署锁这个事可以不提前考虑。4.3 床位状态流转从空床到入住的状态机字段床位管理表面上是简单的字典表实际上藏着状态流转逻辑。t_bed字段不复杂复杂的是状态变化空床可以变为已入住已入住只能变为清洗中清洗中才能再回到空床维修中则任何状态都不可预占。如果代码里到处用if判断状态改需求时你会想骂人。常见做法是在领域层定义一个状态机工具类或者用字段配合Service层校验。表结构如下CREATE TABLE t_bed ( id bigint NOT NULL AUTO_INCREMENT, room_no varchar(16) NOT NULL COMMENT 房号如A101, bed_no varchar(16) NOT NULL COMMENT 床号如A101-1, bed_type varchar(16) NOT NULL DEFAULT DOUBLE COMMENT 单人间SINGLE/双人间DOUBLE/多人间MULTI, status varchar(16) NOT NULL DEFAULT FREE COMMENT FREE-空床 OCCUPIED-已入住 CLEANING-清理中 REPAIR-维修中, current_elder_id bigint DEFAULT NULL COMMENT 当前入住老人ID没入住则为空, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_room_no (room_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT床位表;current_elder_id这个字段是冗余设计它本质可以从t_elder.bed_id反查但保留双向冗余会让查询很方便管理员看床位列表时直接连表查出老人姓名不用再走一遍老人档案。代价是入住和换床时要同时更新两张表必须放在同一个事务里。我建议把“入住登记”这个操作封装成一个Service方法步骤如下校验床位状态为FREE校验老人状态为CREATED用UPDATE t_bed SET statusOCCUPIED, current_elder_id? WHERE id? AND statusFREE做乐观锁更新受影响行数为1才执行老人表的CHECKED_IN更新。这里SQL里的AND statusFREE就是防止并发冲突的乐观锁写法比先查询再更新更安全。如果受影响行数为0说明床位刚被其他人抢占了直接抛出业务异常“该床位已被占用”。换床操作同理旧床位更新为CLEANING新床位更新为OCCUPIED老人表的bed_id同步修改。状态机里最容易被忽略的是CLEANING状态很多项目让床位直接从OCCUPIED变FREE结果保洁还没来得及消毒新老人就住进去了卫生检查时直接翻车。5. 避坑指南养老系统开发中最常见的5个坑5.1 敏感数据明文存储与日志打印泄露现象数据库里id_card字段直接存明文身份证号列表页接口返回全部老人信息操作日志里打印了完整的手机号。原因项目初期图省事没做加密和脱敏接口返回时用实体直接序列化。解决数据库层加密存储比如AES加密Java实体里标记JsonSerialize(using DesensitizedSerializer.class)在序列化时把身份证中间8位换成********日志打印时禁止直接输出实体对象改用toStr()方法手动拼需要显示的字段。另外接口返回给前端的数据要用DTO不要把Elder实体直接返回这样即使前端拿到响应也看不到未脱敏字段。5.2 MyBatis-Plus实体与表名映射不一致现象代码生成后启动项目任何对Elder表的操作都报“Table zhongyang_db.elder doesnt exist”。原因数据库表名是t_elder而实体类名是ElderMyBatis-Plus默认按实体名转下划线找表找到的是elder。解决在实体类上标注TableName(t_elder)。如果表统一有t_前缀更优做法是在全局配置里设置table-prefix: t_这样所有实体都不用手动标注。同样字段名校验也是如此elderName默认映射elder_name如果你的表字段叫old_name就得加TableField(old_name)。养成建表时统一命名规范的习惯比事后加注解省心得多。5.3 Java 8时间类型与数据库时区错乱现象保存LocalDateTime后数据库里时间比当前时间早8小时或者查询出来的时间格式是一串数字。原因数据库连接串没有配置serverTimezoneAsia/ShanghaiMySQL驱动默认用JVM时区容器时区是UTC时就产生偏移。解决JDBC URL务必带serverTimezoneAsia/Shanghai并在MySQL服务器配置里设置default-time-zone 08:00。前端拿到的时间如果是一串数字是因为JSON序列化默认把LocalDateTime转成了时间戳处理方式是Spring Boot配置里加Jackson的LocalDateTime格式化为yyyy-MM-dd HH:mm:ss。这个坑最烦人的地方是不报错只有当你对比创建时间和数据库当前时间时才发现差了8小时。5.4 费用计算用double导致精度丢失现象费用明细表里算“月费 护理费 伙食费”显示结果是199.99999999997用户来投诉算错钱。原因Java的double按二进制浮点数运算很多十进制小数没法精确表示。解决所有金额字段在数据库层用decimal(10,2)Java实体用BigDecimal计算时统一用BigDecimal.add()或multiply()不要用号直接加。还有一个隐藏坑double转BigDecimal时要用BigDecimal.valueOf(double)不要用new BigDecimal(double)后者会保留二进制误差。在费用结算的Service方法上用Transactional保证“生成账单 更新欠费状态”两步同时成功避免只生成账单但没同步欠费余额这种脏数据。5.5 权限拦截器只拦页面不拦接口现象前端把“护理记录操作”按钮隐藏了但用Postman直接调用POST /api/care/execution依然能新增数据。原因权限控制只做了前端菜单按角色渲染后端接口没有任何校验。解决在后端加一个HandlerInterceptor在preHandle里从请求头取出用户信息校验该用户角色是否有权访问当前请求路径。具体做法是维护一个roleApiMap比如nurse角色允许/api/care/**manager角色只允许/api/report/**的GET请求。拦截器里校验失败返回401前端再根据状态码跳转登录页。这里的注意点是拦截器配置里一定要排除登录接口、静态资源和Swagger文档路径否则项目启动后连登录页都进不去。6. 把项目跑起来快速验证与进阶改造的3个技巧6.1 用Mock数据脚本在5分钟内填满演示环境养老系统的报表页、床位状态页如果只有两条测试数据根本看不出效果。我建议在项目里放一个>Component public class DataInitializer implements ApplicationRunner { private final ElderService elderService; private final BedService bedService; Override public void run(ApplicationArguments args) { if (elderService.count() 0) { return; // 已有数据则不重复初始化 } for (int i 1; i 50; i) { Elder elder new Elder(); elder.setElderName(String.format(测试老人%02d, i)); elder.setIdCard(410101 String.format(%012d, i)); elder.setStatus(CHECKED_IN); elder.setHealthLevel(i % 3 0 ? FULL_CARE : SELF_CARE); elderService.save(elder); // 随机分配一个空闲床位并调用入住服务改变床位状态 Bed freeBed bedService.findFirstFreeBed(); bedService.checkIn(freeBed.getId(), elder.getId()); } } }这段代码在项目启动后自动执行if (elderService.count() 0)是防止每次启动都重复灌数据。实际调试阶段你可能希望随时重置数据更灵活的做法是把这段初始化逻辑抽成一个GetMapping(/dev/init)接口只在dev环境配置里开放。参数说明findFirstFreeBed返回的床位要按LIMIT 1查询比随机选一张效率高。每生成一个老人就调用一次checkIn能顺便验证你第4章写的床位状态机代码是否正常一箭双雕。6.2 从单体到前后端分离改造点在哪这套系统的源码如果是用Thymeleaf模板渲染的页面你想改成Vue/React前后端分离项目不要在原有模板上一点一点抠。正确步骤是先梳理现有Controller凡是返回ModelAndView的改为返回统一Result对象静态资源交给前端工程托管后端只保留/api/**接口前端用Vue3 Element Plus重新搭一套管理后台。改造的关键在两点一是统一响应结构比如{code: 200, data: ..., msg: ok}前端拦截器统一处理code ! 200的情况二是跨域问题本地开发时后端配置CorsFilter允许http://localhost:5173访问上线时把前端静态文件部署在Nginx由Nginx反向代理到后端接口这样就没有跨域烦恼。如果你不想前端工程化太重也可以用Spring Boot Thymeleaf直接渲染页面但每个页面都要引入th:fragment做公共头部和侧边栏改一个菜单入口要动所有模板。我做这类中小系统时更倾向于前后端分离前端独立维护后端接口用Swagger文档管理新来的实习生也容易上手。这套源码如果已经是纯接口形式那恭喜你省去了一半改造时间。6.3 报表导出用EasyExcel而不是手写POI院长月底要Excel报表你如果直接用Apache POI手动创建XSSFWorkbook代码量大不说列宽、合并单元格这些样式调起来能把人逼疯。常见做法是集成EasyExcel阿里开源的Excel处理库在模板类上用注解定义列名和格式导出代码非常精简// ElderReportVO.java Data public class ElderReportVO { ExcelProperty(老人编号) private String elderNo; ExcelProperty(姓名) private String elderName; ExcelProperty(健康等级) private String healthLevel; ExcelProperty(入住时间) DateTimeFormat(yyyy-MM-dd HH:mm:ss) private LocalDateTime checkInTime; }// ReportController.java GetMapping(/export/elder) public void exportElder(HttpServletResponse response) throws IOException { response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setCharacterEncoding(utf-8); // 防止文件名乱码用URLEncoder编码 String fileName URLEncoder.encode(老人信息表, UTF-8).replaceAll(\\, %20); response.setHeader(Content-disposition, attachment;filename*utf-8 fileName .xlsx); ListElderReportVO list elderService.listReportData(); EasyExcel.write(response.getOutputStream(), ElderReportVO.class).sheet(老人信息).doWrite(list); }这段代码里ExcelProperty直接指定Excel列名DateTimeFormat控制时间列显示格式。导出性能上EasyExcel使用SAX模式逐行写比POI全量加载省内存一万条数据导出也不会OOM。如果你要按条件导出数据量很大建议加一个asyncExport接口把taskId返回给前端后台用线程池生成文件生成完成后通知前端下载。对这套养老系统来说按床位状态导出、按费用月份导出是院长用着最舒服的两个功能。我在做这类管理系统时最后一步永远是“拿真实数据跑一遍月末结算”。代码写得再漂亮跨月时老人退住、换床、加护理项导致账单不一致才是真正检验设计的地方。如果你照着这套思路复现了中州养老系统建议先让护理计划自动生成跑一周每天抽查几条执行记录和费用数据确认没有脏数据后再给用户用。希望这些踩坑经验能帮你少走几段弯路祝你的养老系统早日上线。本文还有配套的精品资源点击获取