资讯详情 SpringBoot老人健康信息管理系统开发实战:从数据库设计到预警功能实现
📅 2026/10/3 9:52:51
1. 需求拆解这类系统到底在解决什么问题先说一个实际场景。很多做过养老机构、社区健康驿站项目的朋友应该都有同感老人健康信息管理这类系统本质上不是“写代码难”而是“把业务边界梳理清楚难”。一个老人从入住到日常照护涉及的信息包括基础档案、体检记录、慢病随访、用药情况、家属联系方式、护工排班等等如果这些数据散落在Excel、纸质台账和微信聊天记录里一旦要调取某个老人的完整健康轨迹往往要翻半天而且数据很容易丢失或更新不及时。springboot老人健康信息管理系统要解决的就是把这些分散信息集中到一个平台上让医护人员能快速查到老人的健康档案、最近体检结果、慢病管理进度让家属能通过系统或接口看到老人的健康动态让管理员能统计整个机构的健康数据趋势。换句话说这套系统的核心价值是**“让健康数据流转起来”**而不是简单做一个增删改查的CRUD后台。从角色来看这类系统通常有三类用户管理员/医护人员负责老人档案的建立和维护、体检数据录入、慢病随访登记、异常指标的审核处理。老人本人实际上老人直接操作系统的比例很低更多是查询个人健康报告、查看用药提醒所以老人端的功能要尽量简单界面字号要大、操作步骤要少。家属通过账号查看家里老人的健康档案、体检趋势、预警通知这个角色的功能侧重于“查询”和“接收通知”不需要太多的写操作。我记得有一次和社区健康站的工作人员聊需求对方说了一句很实在的话“我们最需要的不是花哨的图表而是能少填几遍表老人来了能快速找到他上次的血压记录。”这句话对系统设计的启发很大业务痛点决定了功能优先级而不是技术炫技。基于这个理解系统的功能模块大致可以划分为下面几块模块核心功能面向角色老人档案管理新增/修改/查询老人基本信息、家属联系方式、入住状态管理员、医护人员体检记录管理录入体检数据、历史记录查询、指标对比医护人员慢病随访管理慢病类型登记、随访计划、随访结果记录医护人员、管理员异常预警提醒指标超限自动识别、推送预警信息管理员、家属系统管理用户账号、角色权限、操作日志管理员这套模块划分的底层逻辑是“人-健康事件-健康管理动作”三层结构先有老人的基础档案再在档案之上累积体检、随访等健康事件最后针对事件产生管理和干预动作预警、通知、随访计划。后续所有接口设计和数据表设计都围绕这条主线展开。2. 技术选型与工程搭建springboot版本怎么选ORM用哪个2.1 springboot版本选择2.7.x还是3.x做这类管理系统技术选型的第一件事就是springboot版本。如果你去搜相关毕设或者企业项目的代码会发现大量教程和开源项目还是基于springboot 2.x这是有原因的。一方面springboot 2.7.x对JDK 8的支持非常稳定而很多云服务器、教学环境、旧项目的JDK还停留在1.8升级JDK 17甚至21的成本不一定值得另一方面springboot 3.x底层是Spring Framework 6javax命名空间改成了jakarta很多老依赖比如某些版本的druid、pagehelper需要换适配版本中间遇到问题的概率会高不少。我的建议很直接如果只是做中小规模的老人健康管理系统没有特别需要虚拟线程、GraalVM这些新特性的场景优先选springboot 2.7.18 JDK 8这套组合踩坑成本最低网上资料也最多。当然如果你是新项目且团队成员对JDK 17已经比较熟悉springboot 3.2.x也完全可以但要注意数据库驱动、连接池、分页插件这些依赖的版本兼容性。用一个表格来对比更清楚对比项springboot 2.7.xspringboot 3.xJDK要求JDK 8及以上最低JDK 17命名空间javax.*jakarta.*社区资料非常丰富逐步完善老项目整合兼容性好需要改依赖虚拟线程不支持支持推荐场景大多数管理系统、毕设、传统企业项目新项目、追求新特性的团队2.2 ORM选型MyBatis还是MyBatis-Plus关于数据访问层老人健康信息管理系统的表结构不算特别复杂但涉及的动态查询条件比较多按姓名、按年龄段、按慢病类型、按时间范围筛选所以ORM选型直接影响开发效率。我个人的经验是优先MyBatis-Plus。原因很简单这类管理系统的大部分操作还是单表CRUD老人档案表、体检记录表、随访表MyBatis-Plus的BaseMapper直接提供insert、updateById、selectPage等方法写起来非常快不需要为每个表写一堆XML。而遇到多表关联查询比如查询老人最近一次体检记录同时带出档案信息用自定义SQL加Select注解或者XML就能搞定也能保留MyBatis的灵活性。有些人对MyBatis-Plus有顾虑觉得“侵入性太强”“不如原生MyBatis可控”实际上对于这个体量的项目完全不用担心。它只是增强不是替换你完全可以继续写自己的SQL。还有一个现实理由你在网上找到的大部分springboot管理系统的开源代码用的都是MyBatis-Plus有问题好查、好问。2.3 前端方案前后端分离还是模板渲染这一块值得多说两句。老人健康信息管理系统既可以用vue springboot做前后端分离也可以用Thymeleaf直接渲染页面。两种方案各有适用场景前后端分离适合有独立前端人员、系统后续要扩展App或小程序接口的情况。vue打包后的静态文件可以放在springboot的resources/static下也可以部署到nginx后端只提供RESTful接口。Thymeleaf服务端渲染适合一个人全栈开发、系统功能以后台管理为主的情况不需要处理跨域、token等前后端联调问题开发链路短。如果你是自己开发时间又比较紧我的建议是先想清楚有没有“对外提供接口给家属端App”的需求。如果有直接走前后端分离后端接口写好后面做小程序或App都方便如果纯粹是机构内部使用后台管理界面为主Thymeleaf完全够用没必要为了“前后端分离”而分离。下面给出一份基于springboot 2.7.18 MyBatis-Plus Thymeleaf方案的pom核心依赖方便直接抄dependencies !-- springboot web启动器 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Thymeleaf模板 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency !-- MyBatis-Plus -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- MySQL驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- Lombok减少实体类代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- Hutool工具类 -- dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.22/version /dependency /dependencies说明一下为什么加Hutool这类系统经常要生成编号、格式化日期、做简单的excel导入导出Hutool能省不少事。比如老人档案批量导入用Hutool的ExcelReader读Excel就比POI原生API方便太多。2.4 application.yml配置要点工程搭建过程中配置文件是关键这里直接给一份相对完整的配置并解释几个容易踩坑的点server: port: 8080 servlet: context-path: / spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/elder_health?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: your_password hikari: maximum-pool-size: 15 minimum-idle: 5 connection-timeout: 30000 mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml type-aliases-package: com.example.elderhealth.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: assign_id spring.thymeleaf: cache: false几个配置细节背后的原因serverTimezoneAsia/ShanghaiMySQL 8驱动要求显式设置时区不设置很可能报“The server time zone value is unrecognized”的错。map-underscore-to-camel-case: true数据库字段用snake_case如health_status实体类用驼峰healthStatus开启后自动映射少写很多resultMap。id-type: assign_idMyBatis-Plus默认的雪花idLong类型主键适合这类业务不用关心主键生成。Thymeleaf的cache: false开发阶段必须关缓存否则改个HTML要重启服务才能看到效果非常影响效率。还有一点提醒如果你的项目中同时引入了spring-boot-starter-security那么所有接口默认会被拦截记得配置放行路径或者把登录认证单独设计好避免一路403。3. 数据库设计老人健康业务的核心表怎么建数据库表设计决定这个系统能走多远也是很多新手最容易翻车的地方。以我的经验老人健康信息管理系统的核心表不需要太多把下面这几张表设计好业务就能完整跑起来老人档案表、用户表、体检记录表、慢病随访表、预警规则表。3.1 老人档案表业务的主索引老人档案是系统的核心主数据几乎所有其他表都通过elder_id关联它。字段设计上要兼顾“信息完整”和“易扩展”CREATE TABLE elder_info ( id bigint(20) NOT NULL COMMENT 主键ID, elder_no varchar(32) NOT NULL COMMENT 档案编号, name varchar(50) NOT NULL COMMENT 姓名, gender tinyint(1) DEFAULT NULL COMMENT 性别 1男 2女 0未知, birth_date date DEFAULT NULL COMMENT 出生日期, id_card varchar(18) DEFAULT NULL COMMENT 身份证号, phone varchar(20) DEFAULT NULL COMMENT 联系电话, emergency_contact varchar(50) DEFAULT NULL COMMENT 紧急联系人, emergency_phone varchar(20) DEFAULT NULL COMMENT 紧急联系人电话, address varchar(255) DEFAULT NULL COMMENT 住址, blood_type varchar(10) DEFAULT NULL COMMENT 血型, allergy_history varchar(255) DEFAULT NULL COMMENT 过敏史, chronic_diseases varchar(255) DEFAULT NULL COMMENT 慢病情况(逗号分隔多个), nursing_level varchar(20) DEFAULT NULL COMMENT 护理等级 自理/半自理/全护理, status tinyint(1) DEFAULT 1 COMMENT 状态 1在住 0已离开, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_elder_no (elder_no), KEY idx_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT老人基本信息表;需要注意两个细节。第一chronic_diseases字段虽然用逗号分隔存了多个慢病但这是为了方便列表展示和快速录入真正的慢病管理还是要靠独立的随访表来做不能把字段存储当成业务模型。第二id_card虽然是唯一标识但不建议直接作为主键因为涉及隐私展示时需要脱敏处理用自增或雪花id做主键更灵活。3.2 体检记录表一个设计难点体检记录表的设计比很多人想的复杂。老人可能在不同时间做不同类型的体检今天量血压明天查血糖后天做年度体检。如果把所有指标都做成固定字段收缩压、舒张压、血糖、血脂…遇到新增检测项目就得改表结构非常痛苦。这就要用到“头表明细表”的设计模式。头表记录“某次体检的基本信息”体检时间、体检类型、体检机构、有无异常明细表记录“具体指标项和值”CREATE TABLE physical_exam ( id bigint(20) NOT NULL COMMENT 主键, elder_id bigint(20) NOT NULL COMMENT 老人ID, exam_date date NOT NULL COMMENT 体检日期, exam_type varchar(30) DEFAULT NULL COMMENT 体检类型 年度/季度/入托/日常, exam_org varchar(100) DEFAULT NULL COMMENT 体检机构, result_summary varchar(500) DEFAULT NULL COMMENT 总体结论, doctor_advice varchar(500) DEFAULT NULL COMMENT 医生建议, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_elder_date (elder_id, exam_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT体检记录头表; CREATE TABLE exam_item_detail ( id bigint(20) NOT NULL, exam_id bigint(20) NOT NULL COMMENT 体检记录ID, item_code varchar(30) NOT NULL COMMENT 指标编码 如 systolic_pressure, item_name varchar(50) NOT NULL COMMENT 指标名称 如收缩压, item_value varchar(50) NOT NULL COMMENT 指标值, item_unit varchar(20) DEFAULT NULL COMMENT 单位 mmHg/mmol/L, ref_range varchar(100) DEFAULT NULL COMMENT 参考范围, is_abnormal tinyint(1) DEFAULT 0 COMMENT 是否异常 1是 0否, PRIMARY KEY (id), KEY idx_exam (exam_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT体检指标明细表;这种设计的好处是新增体检指标时不用改表结构只需在字典里新增一个指标编码。坏处是查询纵向对比时SQL稍微复杂一些比如要看某个老人近半年的血压变化趋势需要做一次透视查询把明细表中的行转成列。MyBatis里可以用条件查询取出数据后在Service层重组也可以直接在SQL里用GROUP BY CASE WHEN做行转列。对于管理系统这种数据量级Service层重组完全够用。3.3 慢病随访表状态是一个动态过程慢病管理不是“登记一次就结束了”而是一个持续过程。很多系统把慢病信息只存一个字段导致随访记录无从谈起。正确的做法是单独建随访表每次与老人的健康沟通、血压测量、用药调整都记录一条随访记录并带上随访时的状态CREATE TABLE chronic_follow_up ( id bigint(20) NOT NULL, elder_id bigint(20) NOT NULL COMMENT 老人ID, disease_type varchar(50) NOT NULL COMMENT 慢病类型 高血压/糖尿病/冠心病等, follow_up_date date NOT NULL COMMENT 随访日期, systolic_pressure int(11) DEFAULT NULL COMMENT 收缩压, diastolic_pressure int(11) DEFAULT NULL COMMENT 舒张压, fasting_blood_glucose decimal(5,2) DEFAULT NULL COMMENT 空腹血糖, medication_status varchar(200) DEFAULT NULL COMMENT 用药情况, life_style varchar(500) DEFAULT NULL COMMENT 生活方式指导, next_follow_date date DEFAULT NULL COMMENT 下次随访日期, doctor_name varchar(50) DEFAULT NULL COMMENT 随访医生, remark varchar(500) DEFAULT NULL COMMENT 备注, PRIMARY KEY (id), KEY idx_elder_disease (elder_id, disease_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT慢病随访记录表;强调一点随访表的核心是“按时间线记录状态变化”。设计时一定要留下next_follow_date这个字段后续做“待随访提醒”的前置查询时直接查这个字段就能筛出即将到期或已逾期的老人不用额外写复杂逻辑。3.4 预警规则表把阈值做成可配置老人健康系统的重头戏之一是异常预警。血压高了要通知医护人员血糖异常要提醒家属。但如果把阈值硬编码在Java代码里后面调整阈值就要改代码重新发版很麻烦。比较好的方式是把预警规则做进数据库CREATE TABLE alert_rule ( id bigint(20) NOT NULL, rule_name varchar(50) NOT NULL COMMENT 规则名称, item_code varchar(30) NOT NULL COMMENT 关联指标编码, operator varchar(10) NOT NULL COMMENT 比较符 BETWEEN, threshold_value varchar(50) NOT NULL COMMENT 阈值 如 140 或 90,140, severity tinyint(1) DEFAULT 1 COMMENT 级别 1提示 2警告 3严重, enabled tinyint(1) DEFAULT 1 COMMENT 是否启用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预警规则表;实际实现时体检数据录入完成后可以遍历启用的规则根据指标编码命中规则的话就生成一条预警记录写入alert_record表同时可以配合短信或公众号通知。这套方案的好处是运营人员可以在后台直接调整阈值不用动代码。4. 核心功能实现从控制器到服务层的完整链路4.1 健康档案模块实现要点老人档案的插入和更新在技术上不复杂但要注意业务校验。比如身份证号格式校验、手机号格式校验、档案编号唯一性校验。我用Hutool的Validator工具类来简化这些校验Override Transactional(rollbackFor Exception.class) public boolean addElder(ElderInfoVO vo) { // 1. 校验身份证号 if (!Validator.isCitizenId(vo.getIdCard())) { throw new ServiceException(身份证号格式不正确); } // 2. 校验手机号 if (StrUtil.isNotEmpty(vo.getPhone()) !Validator.isMobile(vo.getPhone())) { throw new ServiceException(手机号格式不正确); } // 3. 生成档案编号ELD 年月日 随机4位 String elderNo ELD DateUtil.format(new Date(), yyyyMMdd) RandomUtil.randomNumbers(4); vo.setElderNo(elderNo); ElderInfo entity new ElderInfo(); BeanUtil.copyProperties(vo, entity); return elderInfoMapper.insert(entity) 0; }这里有几个值得注意的细节。第一事务注解要加因为可能有后续的初始化操作比如同时生成一条初始健康评估记录保证原子性。第二档案编号生成逻辑虽然简单但并发情况下可能重复可以加唯一索引兜底如果插入时抛出DuplicateKeyException捕获后重新生成编号即可。第三用BeanUtil.copyProperties做VO和实体转换比手动set高效得多但要注意VO里不要包含数据库不存在的字段否则复制到实体会有冗余。查询列表接口通常要支持分页和条件筛选MyBatis-Plus的LambdaQueryWrapper非常合适public PageElderInfoVO pageElders(ElderQueryDTO query) { PageElderInfo page new Page(query.getCurrent(), query.getSize()); LambdaQueryWrapperElderInfo wrapper Wrappers.lambdaQuery(); // 姓名模糊查询 if (StrUtil.isNotBlank(query.getName())) { wrapper.like(ElderInfo::getName, query.getName()); } // 慢病筛选慢性病字段中包含对应关键字 if (StrUtil.isNotBlank(query.getChronicDisease())) { wrapper.like(ElderInfo::getChronicDiseases, query.getChronicDisease()); } // 状态筛选 if (query.getStatus() ! null) { wrapper.eq(ElderInfo::getStatus, query.getStatus()); } // 按创建时间倒序 wrapper.orderByDesc(ElderInfo::getCreateTime); PageElderInfo result elderInfoMapper.selectPage(page, wrapper); // 实体转VO补充年龄等冗余字段 PageElderInfoVO voPage new Page(result.getCurrent(), result.getSize(), result.getTotal()); ListElderInfoVO voList result.getRecords().stream() .map(this::convertToVO) .collect(Collectors.toList()); voPage.setRecords(voList); return voPage; }4.2 体检数据录入事务、校验与趋势查询体检数据录入是系统中写操作最频繁的功能。头表和明细表需要在一个事务里完成写入防止头表插入成功但明细表失败导致脏数据。Controller层接收的DTO结构大致是体检基本信息 指标列表。Service层处理逻辑如下Transactional(rollbackFor Exception.class) public boolean addExamRecord(ExamRecordDTO dto) { // 1. 校验老人存在且状态有效 ElderInfo elder elderInfoMapper.selectById(dto.getElderId()); if (elder null || elder.getStatus() ! 1) { throw new ServiceException(老人信息不存在或已离院); } // 2. 插入头表 PhysicalExam exam new PhysicalExam(); exam.setElderId(dto.getElderId()); exam.setExamDate(dto.getExamDate()); exam.setExamType(dto.getExamType()); exam.setExamOrg(dto.getExamOrg()); physicalExamMapper.insert(exam); // 3. 批量插入明细 ListExamItemDetail items dto.getItems().stream().map(itemDTO - { ExamItemDetail item new ExamItemDetail(); item.setExamId(exam.getId()); item.setItemCode(itemDTO.getItemCode()); item.setItemName(itemDTO.getItemName()); item.setItemValue(itemDTO.getItemValue()); item.setItemUnit(itemDTO.getItemUnit()); item.setRefRange(itemDTO.getRefRange()); // 调用规则判断是否异常 item.setIsAbnormal(checkAbnormal(itemDTO) ? 1 : 0); return item; }).collect(Collectors.toList()); examItemDetailMapper.insertBatch(items); // 4. 触发预警检查 alertService.checkExamAlert(dto.getElderId(), dto.getItems()); return true; }这里要特别说明checkExamAlert的设计思路。异常识别不应该散落在页面代码里而应该集中在服务层。每次体检录入后建议把本次所有指标交给预警服务统一处理预警服务根据实际指标值和阈值配置判断是否生成预警记录。如果有3级严重异常还可以追加一个短信通知的动作。趋势查询这部分最常用的场景是“查看某个老人近6个月血压变化”。由于使用头表明细表结构需要先查出该老人近半年的体检头表ID列表再根据指标编码查到明细。为了减少对数据库的频繁访问可以一次性查出所有明细然后在内存中按exam_id分组public ListTrendPointVO getBloodPressureTrend(Long elderId, int months) { // 1. 计算起始日期 Date startDate DateUtil.offsetMonth(new Date(), -months); // 2. 查询近N月的体检记录ID QueryWrapperPhysicalExam examWrapper new QueryWrapper(); examWrapper.select(id, exam_date) .eq(elder_id, elderId) .ge(exam_date, startDate) .orderByAsc(exam_date); ListPhysicalExam exams physicalExamMapper.selectList(examWrapper); ListLong examIds exams.stream().map(PhysicalExam::getId).collect(Collectors.toList()); if (examIds.isEmpty()) { return Collections.emptyList(); } // 3. 查询这两个指标的所有明细 QueryWrapperExamItemDetail detailWrapper new QueryWrapper(); detailWrapper.in(exam_id, examIds) .in(item_code, systolic_pressure, diastolic_pressure); ListExamItemDetail details examItemDetailMapper.selectList(detailWrapper); // 4. 组装趋势数据 MapLong, ListExamItemDetail map details.stream() .collect(Collectors.groupingBy(ExamItemDetail::getExamId)); // 此处省略具体的值提取和返回组装... }4.3 预警与通知规则引擎的轻量实现很多人一听到“规则引擎”就想到Drools其实在这个场景下完全没必要。用一个简单的“规则表遍历匹配”就够了。核心实现如下public void checkExamAlert(Long elderId, ListExamItemDTO items) { // 加载所有启用的规则 ListAlertRule rules alertRuleMapper.selectList( new LambdaQueryWrapperAlertRule().eq(AlertRule::getEnabled, 1)); if (rules.isEmpty()) { return; } for (ExamItemDTO item : items) { for (AlertRule rule : rules) { if (rule.getItemCode().equals(item.getItemCode()) matchRule(rule, item.getItemValue())) { // 生成预警记录 AlertRecord record new AlertRecord(); record.setElderId(elderId); record.setRuleId(rule.getId()); record.setItemName(item.getItemName()); record.setItemValue(item.getItemValue()); record.setSeverity(rule.getSeverity()); record.setStatus(0); // 0待处理 alertRecordMapper.insert(record); } } } } private boolean matchRule(AlertRule rule, String valueStr) { BigDecimal value new BigDecimal(valueStr); String operator rule.getOperator(); String threshold rule.getThresholdValue(); if (BETWEEN.equals(operator)) { // 阈值格式 90,140 String[] parts threshold.split(,); BigDecimal low new BigDecimal(parts[0]); BigDecimal high new BigDecimal(parts[1]); return value.compareTo(low) 0 value.compareTo(high) 0; } else { BigDecimal thresholdVal new BigDecimal(threshold); switch (operator) { case : return value.compareTo(thresholdVal) 0; case : return value.compareTo(thresholdVal) 0; case : return value.compareTo(thresholdVal) 0; case : return value.compareTo(thresholdVal) 0; case : return value.compareTo(thresholdVal) 0; default: return false; } } }这里提醒一个比较隐蔽的问题判断“异常”和“预警”不一定是同一件事。体检明细表里的is_abnormal字段标识的是“体检指标超出参考范围”而预警表里的“预警”则是“命中专门的预警规则并通知”。两者可能重合但逻辑上是独立的。参考范围来自医疗机构的标准预警规则则由机构自定义这样设计更灵活。4.4 权限控制与家属查询权限这块如果不想引入Spring Security的重量级配置可以考虑用拦截器注解的方式实现简单的角色控制。定义角色枚举ADMIN、DOCTOR、NURSE、FAMILY在Controller方法上加自定义注解RequireRole(ADMIN)拦截器里校验当前登录用户的角色是否匹配。不过实话实说如果系统规模不大我更推荐直接用Spring Security的基础能力只使用它的登录认证和角色放行配置不用oauth2那些复杂内容。核心配置大致是Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers(/login, /css/**, /js/**, /images/**).permitAll() .antMatchers(/admin/**).hasRole(ADMIN) .antMatchers(/doctor/**).hasAnyRole(DOCTOR, ADMIN) .antMatchers(/family/**).hasAnyRole(FAMILY, ADMIN) .anyRequest().authenticated() .and() .formLogin() .loginPage(/login) .defaultSuccessUrl(/index) .permitAll() .and() .logout() .permitAll(); }这里有一个关键点家属的查询范围必须受到严格限制。家属登录后只能查询自己绑定老人的数据不能遍历所有老人。实现方案是在表的关联设计中加入family_bind表家属账号ID和老人ID的多对多关系然后查询时强制带上绑定条件。绑定关系建议在管理员分配账号时建立而不是让家属自助绑定这样能减少授权风险。5. 联调、部署与常见问题这些坑我替你先踩了5.1 打包部署jar包方式最省心这类springboot项目部署优先选择打jar包方式而不是war包丢到Tomcat。jar包内嵌Tomcat部署命令简单也能利用外部配置文件覆盖内部配置。打包时注意几个问题pom.xml中packaging保持默认jar即可不要改成war。如果用了MyBatis-Plus的代码生成器生成代码的目标目录要和maven的sourceDirectory一致否则生成出来的代码不在编译路径中启动时会报找不到Mapper。服务器上启动命令建议加上内存参数java -Xms256m -Xmx512m -jar elder-health.jar --spring.config.locationfile:/opt/elder-health/config/application.yml这样可以在不重新打包的情况下调整生产环境配置。打包后用nohup方式启动nohup java -Xms256m -Xmx512m -jar elder-health.jar app.log 21 如果nginx在前面做反向代理需要额外配置上传文件大小限制。健康档案头像、体检报告图片上传是常见场景SpringBoot默认单文件上传限制1MB很容易出现“文件大小超过限制”的报错修改方式有两种spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB同时nginx的client_max_body_size也要同步调大否则nginx这一层就会直接拒绝。5.2 常见问题速查表做这个系统的过程中有几个问题出现频率特别高整理成表格方便对照排查问题现象可能原因解决方案启动时报数据库时区错误MySQL驱动版本和数据库时区未设置JDBC URL加serverTimezoneAsia/Shanghai查询结果中createTime为null实体字段和数据库列名映射失败确认开启map-underscore-to-camel-case或手动加TableFieldMyBatis-Plus分页查询不生效缺少分页插件配置配置PaginationInnerInterceptorThymeleaf页面修改后不生效开发阶段未关闭缓存spring.thymeleaf.cachefalse并重启上传图片失败超出SpringBoot或nginx大小限制同时调整multipart配置和nginx配置前端跨域请求被阻断前后端分离时未配置CorsFilter编写CorsFilter配置类定时任务不执行漏了EnableScheduling在启动类加EnableScheduling这里重点说一下分页插件MyBatis-Plus从3.5.x版本开始分页插件需要手动添加到MybatisPlusInterceptorConfiguration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }不加这个配置selectPage返回的分页数据永远是total0所有记录都查出来看起来像没分页但实际是分页失效了。5.3 定时任务与首页统计卡片老人健康管理系统还有一个常见的功能首页统计卡片。总老人数、今日新增体检、待随访人数、异常预警数量这些数据如果每次都实时查数据库虽然数据量不大但多个卡片同时查询也存在重复浪费。更合适的方式是写一个定时任务每5分钟或每小时刷新一次统计结果存入Redis或者一个统计表Component Slf4j public class StatisticTask { Autowired private ElderInfoMapper elderInfoMapper; Autowired private PhysicalExamMapper physicalExamMapper; Autowired private AlertRecordMapper alertRecordMapper; Scheduled(cron 0 */5 * * * ?) public void refreshStatistics() { // 调用Mapper统计后存入缓存 log.info(首页统计数据刷新完成); } }注意Scheduled注解要配合EnableScheduling使用。cron表达式里?表示不指定值在“星期”位置用?而不是**会导致每秒执行一次这是一个新手很容易踩的坑。定时任务还要特别留意一个问题如果系统部署在多节点多实例同样的定时任务会在每个节点都执行一遍导致统计数据重复计算或短信重复发送。如果只有单机部署这个问题可以忽略如果是多节点建议用分布式锁或把定时任务单独部署到一个节点上。5.4 一台服务器上部署多个springboot服务的端口管理如果你在本地开发时同时启动了多个springboot项目比如网关、业务服务、可视化面板会出现端口冲突问题。最简单的方式是给每个服务指定不同端口号。但还有一个容易被忽略的地方SpringBoot Actuator的端口。如果你在pom里引入了spring-boot-starter-actuator默认管理端口和应用端口相同如果有多个服务管理端口也会冲突。需要显式设置management: server: port: 8081另外如果用IDEA启动多个服务实例做调试记得在Run Configuration中勾选“Allow parallel run”否则IDEA会直接复用之前的实例新写的代码不会生效。这一类IDE层面的设置问题虽然不直接影响线上但开发效率影响很大。6. 最后的经验总结做这类系统最核心的自我修养就我的实际体会来说springboot老人健康信息管理系统这类项目技术和业务在50%和50%之间。技术部分也就是常规的SSM/SpringBoot CRUD但业务部分的知识深度决定了系统好不好用比如老人体检指标的参考范围什么时候会变化、不同慢病的随访周期应该怎么设、预警级别怎么定义才算合理。这些业务知识不来自代码而是来自你真正到养老机构或者社区卫生站去聊过、看过。一个很实际的建议如果你在开发这类系统建议先找一份真实的体检报告单来看。正常的体检报告单上有几十项指标每一项都有参考范围。把参考范围数据化做成系统里的字典表这是最基础的工作。但这个环节恰恰是很多开发忽视的最后做出来的系统只有血压、血糖、心率三个指标使用方拿到之后会觉得“不够用”又说不清楚缺什么。另外系统的可配置性远比想象的重要。体检指标类型可以增加护理等级可以调整预警阈值不同机构不一样。所以字典表、配置表要多做几个哪怕当前版本只有默认数据。这个习惯会给你后续维护省很多事情。最后这套系统的扩展方向很多。比如对接体检一体机自动采集数据或者做成人脸识别开门后自动关联健康打卡或者给家属开通小程序查询端口。所以开发时接口设计尽量规范字段命名保持统一这样后面接任何外部系统都会顺手很多。写代码的时候想着后面可能要对接但又不能过度设计这个度就是经验所在。