简介:中州养老系统设计源码采用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负责简化配置和内嵌Tomcat,MyBatis负责把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要带参数useSSL=false,否则MySQL 8会警告 DataSourceConfig dataSourceConfig = new DataSourceConfig.Builder( "jdbc:mysql://localhost:3306/zhongyang_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/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接口会继承BaseMapper<Elder>,自带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 中的分页查询 Page<Elder> page = new Page<>(current, size); // current页码从1开始,size每页条数 LambdaQueryWrapper<Elder> wrapper = new LambdaQueryWrapper<>(); wrapper.like(Elder::getElderName, keyword) // 模糊搜索姓名 .eq(Elder::getStatus, "NORMAL") // 只查正常在住状态的老人 .orderByDesc(Elder::getCreateTime); // 按建档时间倒序 Page<Elder> result = elderMapper.selectPage(page, wrapper);说明一下这里的参数:current和size来自前端的pageNum和pageSize,RequestParam接收后直接传给Page。LambdaQueryWrapper的最大好处是类型安全,字段名写错会在编译期报错,而不是运行时才发现“列名不存在”。分页插件会对selectPage自动生成LIMIT ?语句,同时执行一条SELECT COUNT(*)做总数统计。如果列表数据量大,会把COUNT也拖慢,常见优化是给查询条件上的elder_name加普通索引,或者把“仅查未删除数据”的deleted=0条件写进全局拦截器,避免每一条查询都手写。
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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='老人档案表';字段类型选择有几个硬规矩。第一,手机号、身份证号这类“看起来像数字但不会参与计算”的字段,一律用varchar,否则数字太长时MySQL会截断,查询条件也要加引号。第二,性别用tinyint加注释,不用char(1)存“男/女”,否则Java实体里还得维护字符串映射。第三,金额字段不要用float或double,后面5.4会专门说,建表费用表时直接用decimal(10,2)。第四,时间字段用datetime,Java实体里对应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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='床位表';current_elder_id这个字段是冗余设计,它本质可以从t_elder.bed_id反查,但保留双向冗余会让查询很方便:管理员看床位列表时,直接连表查出老人姓名,不用再走一遍老人档案。代价是入住和换床时要同时更新两张表,必须放在同一个事务里。我建议把“入住登记”这个操作封装成一个Service方法,步骤如下:校验床位状态为FREE,校验老人状态为CREATED,用UPDATE t_bed SET status='OCCUPIED', current_elder_id=? WHERE id=? AND status='FREE'做乐观锁更新,受影响行数为1才执行老人表的CHECKED_IN更新。
这里SQL里的AND status='FREE'就是防止并发冲突的乐观锁写法,比先查询再更新更安全。如果受影响行数为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' doesn't exist”。原因:数据库表名是t_elder,而实体类名是Elder,MyBatis-Plus默认按实体名转下划线找表,找到的是elder。解决:在实体类上标注@TableName("t_elder")。如果表统一有t_前缀,更优做法是在全局配置里设置table-prefix: t_,这样所有实体都不用手动标注。同样,字段名校验也是如此:elderName默认映射elder_name,如果你的表字段叫old_name,就得加@TableField("old_name")。养成建表时统一命名规范的习惯,比事后加注解省心得多。
5.3 Java 8时间类型与数据库时区错乱
现象:保存LocalDateTime后,数据库里时间比当前时间早8小时;或者查询出来的时间格式是一串数字。原因:数据库连接串没有配置serverTimezone=Asia/Shanghai,MySQL驱动默认用JVM时区,容器时区是UTC时就产生偏移。解决:JDBC URL务必带serverTimezone=Asia/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"); List<ElderReportVO> list = elderService.listReportData(); EasyExcel.write(response.getOutputStream(), ElderReportVO.class).sheet("老人信息").doWrite(list); }这段代码里@ExcelProperty直接指定Excel列名,@DateTimeFormat控制时间列显示格式。导出性能上,EasyExcel使用SAX模式逐行写,比POI全量加载省内存,一万条数据导出也不会OOM。如果你要按条件导出,数据量很大,建议加一个asyncExport接口:把taskId返回给前端,后台用线程池生成文件,生成完成后通知前端下载。对这套养老系统来说,按床位状态导出、按费用月份导出,是院长用着最舒服的两个功能。
我在做这类管理系统时,最后一步永远是“拿真实数据跑一遍月末结算”。代码写得再漂亮,跨月时老人退住、换床、加护理项导致账单不一致,才是真正检验设计的地方。如果你照着这套思路复现了中州养老系统,建议先让护理计划自动生成跑一周,每天抽查几条执行记录和费用数据,确认没有脏数据后再给用户用。希望这些踩坑经验能帮你少走几段弯路,祝你的养老系统早日上线。
本文还有配套的精品资源,点击获取