基于Java的养老管理系统设计:模块划分、状态流转与并发控制
2026/9/14 20:57:28 网站建设 项目流程

简介:这是一份基于Java技术实现的中州养老项目设计源码,面向Java开发人员与养老行业信息化建设者,旨在借助现代Web开发技术提供高效便捷的养老服务解决方案,缓解人口老龄化背景下的社会服务压力。资源包含116个文件,其中99个Java源文件承载用户管理、数据访问、服务接口等核心业务逻辑,13个XML文件与YAML、JSON、Git忽略文件等共同完成环境配置、数据库连接与版本管理辅助,整包约356KB,结构清晰、模块区分明显。项目采用common、web、service三层模块化设计,common封装通用工具与基础类,web处理界面交互,service专注业务处理,涵盖接口文档配置、登录控制、自动填充拦截等典型后端实现,便于读者理解企业级开发中的配置管理、权限控制与接口封装思路。目前已有331人浏览学习,适合作为课程设计、毕业设计或实际养老信息化项目的参考范本,尤其适合有一定Java基础、希望提升Spring Boot实战能力的开发者研读。

1. 中州养老项目为什么值得用 Java 重新梳理

养老管理系统的开发难度从来不在增删改查,而在业务状态如何随照护过程持续变化。以中州养老项目为代表的一类民政、街道或民办养老机构的综合管理平台,核心是老人从入院评估、床位分配、护理计划执行到费用结算的完整链路。这条链路里每一环的状态都不是孤立的:老人退住要解除床位占用,护理任务跨天要自动生成,账单要跟着入住和退住日期精确计算。用 Java 技术栈来做这类系统,胜在生态成熟、事务和并发工具完备、团队招聘成本低。本文面向已经能写 Spring Boot CRUD 但想了解真实业务系统如何落地的开发者,也面向需要评审这类项目源码的技术负责人,重点说清模块怎么拆、状态怎么设计、代码怎么组织,以及交付后最容易出问题的地方在哪里。

2. 设计源码前的系统骨架:模块划分与 Java 技术选型

2.1 先把业务域拆成六个独立模块再谈代码

拿到“中州养老项目”这类标题时,第一步不是建 Maven 工程,而是把业务域画清楚。一个可落地的养老管理后台通常包含以下模块:

模块核心职责关键实体
档案中心老人基本信息、家属联系人、健康档案、入院评估老人档案、评估记录
照护管理护理计划、每日任务生成、执行反馈护理计划、护理记录
床位管理房间、床位状态、入住退住房间、床位、入住记录
收费管理月度账单、押金、退费计算账单、缴费流水
系统权限员工账号、角色、菜单权限用户、角色、菜单
统计分析入住率、护理完成率、收入报表统计视图、报表

这六个模块之间不是平等的 CRUD 关系。床位管理是基础数据,档案中心是业务起点,照护管理和收费管理是每天都会被高频写入的部分。源码如果按这种边界分包,后续扩展家属端小程序、对接民政监管平台都能做到不动主链路。

2.2 技术选型遵循“单应用 + 缓存 + 关系型数据库”的保守路线

这类养老项目的数据量通常在十万级到百万级,并发峰值集中在早晨交接班和月末结算时段,远没有到需要微服务的程度。我一般会建议按下面的组合来搭基础工程:

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.20.1</version> </dependency> </dependencies>

MyBatis-Plus 在这个项目里的价值是减少单表 CRUD 的样板代码,LambdaQueryWrapper可以直接把查询条件写在类型安全的方法引用上,避免字符串拼 SQL。Redisson 不是必须的,但当护理任务生成、缴费冲正这些操作需要分布式锁时,直接用RLock比自己去 Redis 上写SETNX要稳得多,它内置了看门狗续期机制。Spring Boot 版本建议锁在 2.7.x,这个版本对 JDK 8 和 JDK 11 的兼容性都经过了大量生产环境验证,和 MyBatis-Plus 3.5.x 配合也最成熟。

2.3 工程目录的分层约定从第一个包就定死

com.zhongzhou.pension/ ├── controller/ # HTTP 层,只做参数接收和响应封装 ├── service/ # 业务层,事务边界都在这层 │ └── impl/ ├── mapper/ # MyBatis-Plus 数据访问接口 ├── entity/ # 数据库实体 ├── dto/ # 入参出参对象 ├── common/ # 统一返回体、异常、常量 └── config/ # MyBatis-Plus、Redis、Web 配置

分包逻辑的核心原则是:controller 不写业务,service 不碰 HttpServletRequest,entity 不掺入前端展示逻辑。养老类项目后期需求变更多,比如护理记录要加图片附件、账单要加滞纳金字段,边界清晰才能做到改一个方法不影响整条链路。

提示:常见误把枚举值说明写在 entity 里当注释,更好的做法是单独建enums/包存放状态枚举,SQL 存tinyint,Java 用枚举转换,避免魔法数字散落在各个 Service 方法里。

3. 养老业务的数据底座:核心实体与表结构设计

3.1 老人档案表要把“现住状态”和“历史记录”分开建模

养老项目源码里最容易出问题的是老人档案表设计。很多人把入住状态、退住时间、床位 ID 全塞进elder_info一张表,导致老人每次更换床位都要 UPDATE 主记录,历史痕迹全部丢失。更稳妥的做法是主表只存“当前状态”,明细表存“每一次状态变更”,查询时以主表为准,回溯时查明细表。

CREATE TABLE `elder_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `elder_no` varchar(32) NOT NULL COMMENT '档案编号', `name` varchar(64) NOT NULL COMMENT '老人姓名', `id_card` varchar(18) NOT NULL COMMENT '身份证号', `gender` tinyint(1) NOT NULL COMMENT '1男 2女', `birth_date` date DEFAULT NULL COMMENT '出生日期', `health_level` tinyint(1) DEFAULT NULL COMMENT '照护等级 1自理 2半自理 3全护理', `admission_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0未入住 1在住 2已退住', `current_bed_id` bigint(20) 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`), UNIQUE KEY `uk_id_card` (`id_card`), KEY `idx_admission_status` (`admission_status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='老人档案主表';

这段 DDL 里有三个关键决策。第一,身份证号加唯一索引,避免同一个老人被重复建档;第二,admission_status加普通索引,因为列表页最常见的筛选条件就是“在住/已退住”;第三,current_bed_id允许为空,刚建档未入住的老人不应该有床位绑定。照护等级单独建字段而不是放评估明细里,是因为护理计划生成时要频繁按等级匹配模板,冗余这个字段能省去一次关联查询。

3.2 状态变更流水表是审计和退费计算的依据

CREATE TABLE `elder_status_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `elder_id` bigint(20) NOT NULL, `old_status` tinyint(1) NOT NULL, `new_status` tinyint(1) NOT NULL, `bed_id` bigint(20) DEFAULT NULL, `operator_id` bigint(20) NOT NULL, `remark` varchar(255) DEFAULT NULL, `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_elder_id` (`elder_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='老人入住退住状态流水';

退费计算最怕的是“入住时间说不清”。有了这张流水表,退住操作发生时只需把new_status写成2,并记录当时的床位 ID,费用结算模块按流水时间计算实际占床天数,不依赖任何人在页面上手填日期。这比在业务代码里update elder_info set status = 2之后再手工 insert 一条日志要可靠得多,把状态流转固化在数据库层面。

3.3 MyBatis-Plus 的字段填充和逻辑删除配置

实体类上使用自动填充注解,创建时间和更新时间就不需要每个 Service 方法手动 set:

@Data @TableName("elder_info") public class ElderInfo { @TableId(type = IdType.AUTO) private Long id; private String name; private Integer admissionStatus; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }

IdType.AUTO依赖数据库自增主键,适合 MySQL;如果未来考虑分库分表,再改成ASSIGN_ID(雪花算法)。FieldFill.INSERT_UPDATE配合MetaObjectHandler实现类,插入时自动写入当前时间,更新时自动刷新。逻辑删除统一用@TableLogic注解加deleted字段,但要注意:带唯一索引的业务字段(如身份证号)不能直接依赖逻辑删除,需要在删除前先做一次包含deleted = 1的查重处理,否则再次录入同一个人会报唯一键冲突。

4. 关键业务链路的代码落地:档案、床位与护理计划

4.1 老人档案分页查询:参数封装与响应体设计

@RestController @RequestMapping("/api/elder") public class ElderController { @GetMapping("/page") public Result<IPage<ElderVO>> page(ElderQuery query) { return Result.ok(elderService.page(query)); } }

分页查询的入参对象ElderQuery继承PageParams,父类里固定有pageNumpageSize两个字段,业务子类只加筛选条件。这么设计的理由是:所有查询接口的翻页行为保持一致,前端封装 axios 请求时可以统一携带页码参数,不用为每个接口单独适配。Result<T>统一包裹返回体,包含codemessagedata三个字段,业务异常码集中在ErrorCode枚举里维护,接口层不出现裸字符串提示。

Service 层实现要点:

@Override public IPage<ElderVO> page(ElderQuery query) { Page<ElderInfo> page = new Page<>(query.getPageNum(), query.getPageSize()); LambdaQueryWrapper<ElderInfo> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(query.getName()), ElderInfo::getName, query.getName()) .eq(query.getAdmissionStatus() != null, ElderInfo::getAdmissionStatus, query.getAdmissionStatus()) .orderByDesc(ElderInfo::getCreateTime); IPage<ElderInfo> entityPage = this.baseMapper.selectPage(page, wrapper); return entityPage.convert(elder -> convertToVO(elder)); }

likeeq方法的重载第一参数是boolean condition,条件为 false 时这个查询条件自动跳过,可以避免写一堆if判断。分页对象PagegetRecords()返回当前页数据,getTotal()返回总记录数,convert方法可以在分页结果上直接做实体到视图对象的字段映射,比如把admissionStatus0/1/2转成前端需要的text文案。

4.2 床位分配要避免并发把同一个床位分给两个老人

床位分配是养老系统里最典型的并发争抢场景:两个护工同时办理入住,看到 203 床空闲,如果代码不做控制,两个老人都可能被写入current_bed_id。用数据库层面的乐观锁解决,在床位表加version字段:

@Update("UPDATE bed_info SET status = 2, elder_id = #{elderId}, version = version + 1 " + "WHERE id = #{bedId} AND status = 1 AND version = #{version}") int occupyBed(@Param("bedId") Long bedId, @Param("elderId") Long elderId, @Param("version") Integer version);

调用方拿到 UPDATE 返回的受影响行数,如果是0说明床已被占用或版本号不匹配,这时直接抛出业务异常“当前床位已被占用,请刷新后重试”。乐观锁比SELECT FOR UPDATE好在不用长时间持有数据库连接锁,在养老院内网这种低并发场景下几乎不会产生冲突重试的额外成本。

入住流程的事务边界需要想清楚:occupyBedupdate elder_info必须放在同一个事务方法里,否则床位占了但老人的current_bed_id没写进去,数据就不一致了。

@Transactional(rollbackFor = Exception.class) public void checkIn(CheckInRequest request) { int affected = bedMapper.occupyBed(request.getBedId(), request.getElderId(), request.getVersion()); if (affected == 0) { throw new BusinessException(ErrorCode.BED_OCCUPIED); } elderMapper.occupyBed(request.getElderId(), request.getBedId()); }

4.3 护理计划按模板每天定时生成任务

照护模块的常见做法是:管理员维护护理模板(如全护理每天 6 次翻身、3 次喂餐),每天凌晨由定时任务根据老人的health_level生成当天的护理任务。生成逻辑必须做幂等控制,最直接的方式是任务表加唯一索引:

ALTER TABLE nursing_task ADD UNIQUE KEY uk_elder_date_type (elder_id, task_date, task_type);

定时任务方法执行时先查一下当天是否已有记录,没有才批量插入。即使任务调度平台重复触发,唯一索引也会挡住第二次插入,不会产生同一天同一类型的重复护理项。生成后的任务在 App 端或 PC 端展示给护工,执行完勾选完成,后台记录执行时间、执行人和完成状态。

5. 源码交付后的几个自查项与调优清单

源码交付不是把代码传上去就结束了,我会按下面的清单逐项检查:

第一,Long 型主键序列化到前端是否丢精度。Java 后端返回 JSON 时,Long 超过 16 位会被 JavaScript 截断,前端拿到错误 ID 再去调详情接口就会 404。统一在 Jackson 配置里把 Long 转成 String 输出:

@Bean public Jackson2ObjectMapperBuilderCustomizer longToStringCustomizer() { return builder -> builder.serializerByType(Long.class, ToStringSerializer.instance); }

第二,慢 SQL 排查。在application.yml里开启 MyBatis-Plus 的慢 SQL 日志:

mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

看日志里==> Preparing==> Parameters的输出,如果发现某条 SQL 的WHERE条件字段没有索引,直接补充普通索引。养老项目最常见的慢查询是护理记录按时间范围统计,task_date必须建索引。

第三,定时任务生成当天护理记录时数据量大导致的超时。单次批量插入超过 2000 条建议分片执行,每片 500 条INSERT INTO ... VALUES,避免单条 SQL 过长或事务持有锁时间太久影响前台操作。生成完成后打印日志包含elderId数量和耗时毫秒,方便后续核对。

第四,缓存与数据库一致性。养老服务对数据实时性要求高,不建议在档案查询上启用本地缓存。如果一定要用 Redis 缓存老人信息,更新入口收敛到 Service 层,更新数据库后主动delete缓存而不是更新缓存,下次查询自然回填,用“删除缓存”代替“更新缓存”可以规避并发写缓存导致的数据错乱。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询