拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于Java的养老管理系统设计:模块划分、状态流转与并发控制

简介这是一份基于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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.20.1/version /dependency /dependenciesMyBatis-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 不碰 HttpServletRequestentity 不掺入前端展示逻辑。养老类项目后期需求变更多比如护理记录要加图片附件、账单要加滞纳金字段边界清晰才能做到改一个方法不影响整条链路。提示常见误把枚举值说明写在 entity 里当注释更好的做法是单独建enums/包存放状态枚举SQL 存tinyintJava 用枚举转换避免魔法数字散落在各个 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT老人入住退住状态流水;退费计算最怕的是“入住时间说不清”。有了这张流水表退住操作发生时只需把new_status写成2并记录当时的床位 ID费用结算模块按流水时间计算实际占床天数不依赖任何人在页面上手填日期。这比在业务代码里update elder_info set status 2之后再手工 insert 一条日志要可靠得多把状态流转固化在数据库层面。3.3 MyBatis-Plus 的字段填充和逻辑删除配置实体类上使用自动填充注解创建时间和更新时间就不需要每个 Service 方法手动 setData 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 ResultIPageElderVO page(ElderQuery query) { return Result.ok(elderService.page(query)); } }分页查询的入参对象ElderQuery继承PageParams父类里固定有pageNum和pageSize两个字段业务子类只加筛选条件。这么设计的理由是所有查询接口的翻页行为保持一致前端封装 axios 请求时可以统一携带页码参数不用为每个接口单独适配。ResultT统一包裹返回体包含code、message、data三个字段业务异常码集中在ErrorCode枚举里维护接口层不出现裸字符串提示。Service 层实现要点Override public IPageElderVO page(ElderQuery query) { PageElderInfo page new Page(query.getPageNum(), query.getPageSize()); LambdaQueryWrapperElderInfo wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(query.getName()), ElderInfo::getName, query.getName()) .eq(query.getAdmissionStatus() ! null, ElderInfo::getAdmissionStatus, query.getAdmissionStatus()) .orderByDesc(ElderInfo::getCreateTime); IPageElderInfo entityPage this.baseMapper.selectPage(page, wrapper); return entityPage.convert(elder - convertToVO(elder)); }like和eq方法的重载第一参数是boolean condition条件为 false 时这个查询条件自动跳过可以避免写一堆if判断。分页对象Page的getRecords()返回当前页数据getTotal()返回总记录数convert方法可以在分页结果上直接做实体到视图对象的字段映射比如把admissionStatus的0/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好在不用长时间持有数据库连接锁在养老院内网这种低并发场景下几乎不会产生冲突重试的额外成本。入住流程的事务边界需要想清楚occupyBed和update 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缓存而不是更新缓存下次查询自然回填用“删除缓存”代替“更新缓存”可以规避并发写缓存导致的数据错乱。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门