这两年我前前后后做过几个基于 SpringBoot 的管理系统,但真正让我觉得"这系统做出来是给人用的",还是这个社区智慧养老系统。起因很朴素:有次我陪家里长辈去社区服务站办高龄津贴,看到工作人员还在用 Excel 翻老人的健康档案,几万个格子,眼睛都快看瞎了。旁边一台血压计测完数据,全靠手抄,家属打电话问老人今天血压怎么样,对方得翻五分钟纸质记录才能答上来。
回来后我就想,能不能用 SpringBoot 把这事串起来:老人档案、健康数据、服务工单、告警通知全部数字化。于是就有了这套"基于 SpringBoot 的社区智慧养老系统"。它会解决社区养老服务站最头疼的三件事:老人底数不清、服务过程无记录、健康异常靠人盯。适合刚学完 SpringBoot 想拿真实项目练手的人,也适合正在构思毕业设计的同学,以及想了解这类系统怎么从需求落到代码的开发者。这篇就把完整的技术选型、数据库设计、核心模块实现和上线前踩过的坑,一条条讲清楚。
1. 社区养老的业务盘子究竟有多大:需求剖析与模块边界
想写代码之前,先把业务看清楚。很多做管理系统的人犯的第一个错,就是上来就建表,结果表建了二三十张,业务逻辑却是乱的。智慧养老系统看起来是"管理老人信息",实际上它是一个典型的社区级综合业务平台,涉及的人员、事项、流程相当繁杂。
1.1 系统到底要管哪些事
我先后走访了几个社区服务站,跟一线社工聊完,把业务归纳为四条主线:
第一,老人全生命周期档案。这个不只是姓名、电话、住址,还包含健康等级评估(自理、半失能、失能)、是否独居、紧急联系人、补贴类型、既往病史、用药情况等。这些字段不建好,后面服务派单、告警判断都无从谈起。
第二,健康数据采集与风险预警。社区里有一部分老人配备了智能手环、血压计、血糖仪,设备会定时把数据传到平台。平台要做的不只是"存下来",而是根据阈值判断是否异常,异常了通知谁、以什么方式通知、多长时间没响应要升级处理。
第三,服务工单流转。助餐、助浴、保洁、陪诊、代买药,这些服务的预约、派单、执行、回访、评价,要形成一个完整闭环。这个环节最考验数据库状态设计,因为工单状态一旦设计乱了,统计报表全是错的。
第四,补贴与费用管理。高龄津贴、护理补贴、服务费用减免,牵扯到审批流程。这部分业务规则因地区而异,所以系统设计上要留扩展位,不能写死。
四条主线理完之后,核心用户角色也浮现出来了:系统管理员(管账号和配置)、社区工作人员(社工/网格员,日常操作主体)、服务人员(承接工单的人)、老人家属(查看老人情况、接收告警)、老人本人(通过终端/小程序简易交互)。家属这个角色最容易漏掉,但恰恰是养老系统最有价值的服务对象之一。
1.2 一期功能边界:哪些必须做,哪些放二期
需求调研出来,能列的功能两三页纸都写不完。如果全做,工期会失控,所以我做了一期和二期切割。
一期必须做的是闭环核心:老人档案管理、健康档案管理、服务工单管理、告警管理、账号权限管理、数据统计看板。这六个模块能支撑社区服务站跑通"建档—监测—服务—回访"的完整链路。
二期再考虑:家属/老人移动端(小程序、App,用来查档案、接收消息)、IoT 设备自动接入与心电图等多维数据、政务对接与第三方支付对账。这些东西不是不重要,而是可以等业务跑起来之后,用实际数据反推需求再设计,比一次性做一堆没人用的功能强得多。
这里我总结了一张角色功能矩阵,设计菜单和权限时直接照着分:
| 功能模块 | 系统管理员 | 社区工作人员 | 服务人员 | 老人家属 |
|---|---|---|---|---|
| 老人档案 | 查看/导出 | 新增/编辑/查看 | 仅查看被服务对象 | 仅查看本人亲属 |
| 健康档案与告警 | 查看全部 | 查看/处理告警 | 不涉及 | 接收通知/查看 |
| 服务工单 | 查看/统计 | 创建/派单/回访 | 接单/提交完成 | 查看进度 |
| 账号权限 | 完整管理 | 查看本人 | 不涉及 | 不涉及 |
| 统计看板 | 全部数据 | 本网格数据 | 不涉及 | 不涉及 |
模块边界清晰之后,建表和写接口才有了依据。接下来就是技术选型的问题。
2. 技术选型不是越新越好:基于 SpringBoot 的这套组合为什么够用
技术选型这件事,我的态度一直很务实:先看项目规模和运行环境,再决定用什么。社区智慧养老系统的典型部署环境是:街道/社区的一台普通服务器,预算不高,网络环境一般,并发量不大(一个街道几百个并发就到顶了),但业务逻辑复杂、角色权限多、后续可能有移动端和 IoT 设备接入。这种场景下,SpringBoot 单体应用就是最合适的形态。
2.1 框架选型的取舍逻辑
我用的版本组合是SpringBoot 2.7.18 + JDK 1.8 + Maven 3.8。有人会问,都什么年代了还用 JDK 8?原因很实在:很多社区机房里还有老系统,运维方的 JDK 环境不一定是新的,JDK 8 的兼容性最稳。而且 SpringBoot 2.7 是 2.x 生命周期里最后一个支持 JDK 8 的稳定版本,社区资料多,遇到问题一搜全有答案。
如果你是用 JDK 17 起步,那可以直接上 SpringBoot 3.x,但注意 MyBatis-Plus、某些短信 SDK 的兼容版本要对应升级,不然启动直接报错。我个人建议,除非你明确知道要在新环境下部署,否则SpringBoot 2.7.x 是最保守稳妥的选择。
持久层我选了 MyBatis-Plus,不是 JPA。理由有三点:
- 社区养老系统有大量多表联查和部分字段更新的操作,MyBatis-Plus 的 LambdaQueryWrapper 写起来直观,复杂 SQL 又可以手写,灵活度高。
- 团队里新人上手快,MyBatis 系是国内 Java 开发者的基本功底。
- 分页插件非常成熟,列表页几乎都要分页,用它省很多事。
JPA 在简单的 CRUD 上确实优雅,但在复杂查询和多人协作时,Hibernate 的实体映射和懒加载问题容易变成坑。这个项目业务规则复杂、报表查询多,选 MyBatis-Plus 更合适。
2.2 权限框架、缓存与其他组件
权限这块我用的是Spring Security + JWT。系统里有五类角色,菜单权限、按钮权限、数据权限都要控制,Spring Security 的过滤器链机制能很好地承担这个职责。JWT 做无状态登录,接口返回一个 token,前端统一携带,不需要在服务端维护 session,比较省事。
Redis 在这个系统里不是必需品,但用了之后体验提升明显。我主要拿它做两件事:一是缓存菜单权限和老人档案热点数据,减少数据库压力;二是做接口防重提交的分布式锁(后面详细讲)。如果服务器实在紧张,2G 内存的机器也能跑,Redis 和数据都在一台机器上,配置好 JVM 参数即可。
MySQL 用的 5.7,原因是老服务器上现成的版本就是 5.7,够用。字段全部用 utf8mb4,不然手机端家属名字带个生僻字就存不进去,这种事我踩过。
文件存储方面,一期我没有搞对象存储,先做成本地磁盘存储,配置一个 upload 目录映射为静态资源。如果以后部署到云上,再切换到 OSS,接口层预留了 FileService 接口,切换成本很低。
最终选型清单如下:
| 技术点 | 选型 | 选型理由 |
|---|---|---|
| 开发框架 | SpringBoot 2.7.18 | 稳定成熟、兼容 JDK8、资料丰富 |
| JDK | JDK 1.8 | 兼容老环境,部署风险最低 |
| 持久层 | MyBatis-Plus 3.5.x | 灵活、易上手、分页插件好用 |
| 权限 | Spring Security + JWT | 多角色菜单+按钮+数据权限都够用 |
| 缓存 | Redis 5.x | 热点缓存 + 防重锁 |
| 数据库 | MySQL 5.7 | 现有环境、稳定够用 |
| 接口文档 | knife4j | 前后端联调方便、生成的文档可以给社区方留档 |
| 部署方式 | 单机 JAR + systemd | 运维简单,升级只需要换包重启 |
这里多说一句:有些团队一上来就想上 Spring Cloud Alibaba、Nacos 注册中心、网关网关的,一个社区项目完全没必要。微服务解决的是协作和扩容问题,不是业务复杂度问题。单体应用把模块边界划清楚,后面真要拆,也是水到渠成的事。
3. 数据库模型设计:老人档案、服务工单与健康数据的关联玩法
数据库是整个系统的地基。我在设计表结构时,核心思路是:档案表做全、业务表做净、关联表做稳。档案表把该有的信息都放进去,业务表只管业务字段,关联关系用外键逻辑管理(实际建表不一定要物理外键,但逻辑关系必须明确)。
3.1 核心表结构拆解
先看老人档案表。这个表我命名为elderly_info,字段设计是踩过几轮坑之后定下来的:
CREATE TABLE `elderly_info` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `elderly_no` VARCHAR(32) NOT NULL COMMENT '老人编号,业务唯一', `name` VARCHAR(64) NOT NULL COMMENT '姓名', `id_card` VARCHAR(18) NOT NULL COMMENT '身份证号,加密存储', `gender` TINYINT NOT NULL COMMENT '性别 1男 2女', `birthday` DATE NOT NULL COMMENT '出生日期', `phone` VARCHAR(20) DEFAULT NULL COMMENT '本人电话', `address` VARCHAR(255) DEFAULT NULL COMMENT '居住地址', `emergency_contact_name` VARCHAR(64) DEFAULT NULL COMMENT '紧急联系人姓名', `emergency_contact_phone` VARCHAR(20) DEFAULT NULL COMMENT '紧急联系人电话', `relation` VARCHAR(20) DEFAULT NULL COMMENT '与老人关系', `health_level` TINYINT DEFAULT 1 COMMENT '健康等级 1自理 2半失能 3失能', `live_status` TINYINT DEFAULT 1 COMMENT '居住情况 1独居 2与家人同住 3其他', `subsidy_type` VARCHAR(100) DEFAULT NULL COMMENT '补贴类型,逗号分隔', `medical_history` TEXT COMMENT '既往病史', `medication_info` TEXT COMMENT '用药情况', `area_code` VARCHAR(20) DEFAULT NULL COMMENT '所属网格/片区编码', `status` TINYINT DEFAULT 1 COMMENT '状态 1正常 0注销', `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', `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_elderly_no` (`elderly_no`), KEY `idx_area_code` (`area_code`), KEY `idx_health_level` (`health_level`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='老人档案表';身份证号是敏感信息,生产环境我做了AES 加密后再入库,查询时按需解密展示,而不是明文存储。手机号同样处理。这个细节如果漏掉,等安全审查的时候会被打回来重做。
健康档案表health_record是这样设计的:
CREATE TABLE `health_record` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `elderly_id` BIGINT NOT NULL COMMENT '老人ID', `record_type` VARCHAR(20) NOT NULL COMMENT '指标类型:blood_pressure/blood_sugar/heart_rate', `systolic_pressure` INT DEFAULT NULL COMMENT '收缩压(血压)', `diastolic_pressure` INT DEFAULT NULL COMMENT '舒张压(血压)', `blood_sugar_value` DECIMAL(5,1) DEFAULT NULL COMMENT '血糖值', `heart_rate_value` INT DEFAULT NULL COMMENT '心率', `measure_time` DATETIME NOT NULL COMMENT '测量时间', `source` TINYINT DEFAULT 1 COMMENT '来源 1设备 2手动录入', `device_no` VARCHAR(50) DEFAULT NULL COMMENT '设备编号', `is_abnormal` TINYINT DEFAULT 0 COMMENT '是否异常 1是 0否', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_elderly_time` (`elderly_id`, `measure_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康指标记录表';这里的is_abnormal字段是后端计算后落库的,不是查询时即时判断。原因有两个:一是告警逻辑随时间会调整,落库保留当时判断结果,后续统计口径一致;二是列表页按异常筛选走索引,不用每次联表算。
3.2 服务工单的状态流转设计
服务工单表service_order是这个系统里业务最复杂的表。我设计的字段大致是:老人ID、服务类型ID、服务人员ID、预约时间、完成时间、费用、状态、评价内容、创建人、派单时间、回访时间、回访结果。
关键是状态字段。我定的状态枚举为:0待派单 → 1待服务 → 2服务中 → 3已完成,另有4已取消和5已驳回两个终态。流转规则如下:
- 创建工单后状态为
待派单; - 工作人员指派服务人员时变为
待服务; - 服务人员接单(可选,如果强制派单则跳过)后进入
待服务; - 服务人员点击"开始服务"变为
服务中; - 完成服务变为
已完成; - 创建人可取消未开始的单(
待派单/待服务状态),服务人员也可申请驳回(要有驳回原因字段); - 已完成之后由工作人员回访,回访结果放在单独的
visit_record表,不改工单主状态。
状态流转必须由代码控制,不允许直接 UPDATE 状态。我封装了一个状态校验方法,每次更新前检查当前状态是否在合法流转路径上。这里有一个很容易犯的错:允许从任意状态改成已完成。这样最后统计服务完成率的时候,数据会莫名其妙地好,但实际上是脏数据。
另外一个容易忽略的点是工单号。工单号要具有可读性,比如FW20240512001,前缀 FW + 日期 + 当天自增序号。千万别用自增主键直接展示给用户,否则社区的工作人员在微信里对工单号的时候,根本记不住也看不出是哪天的单子。
4. 后端关键模块的实现细节:从登录鉴权到健康告警
骨架搭好了,接下来是血肉。这一节我挑三个最有代表性的模块讲实现:多角色登录与权限控制、健康数据异常判定与告警推送、服务工单闭环流转。这三个模块几乎覆盖了系统一半的逻辑复杂度。
4.1 多角色统一登录与权限控制的落地
系统有五类角色,登录入口是统一的。登录成功之后,后端返回 JWT token,token 里只放用户ID、用户名、角色编码三个核心信息,不放大对象。
JWT 工具类这样写:
@Component public class JwtUtil { @Value("${jwt.secret}") private String secret; @Value("${jwt.expire}") private Long expire; public String generateToken(Long userId, String username, String roleCode) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("username", username) .claim("role", roleCode) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + expire)) .signWith(SignatureAlgorithm.HS256, secret.getBytes(StandardCharsets.UTF_8)) .compact(); } public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret.getBytes(StandardCharsets.UTF_8)) .parseClaimsJws(token) .getBody(); } }注意secret不能写在代码里,放在 application.yml 中通过配置项读取,生产环境用环境变量注入。这个细节被安全工具扫描出来过,所以专门提醒。
权限控制我采用的是 Spring Security 过滤器链里加一个 JWT 认证过滤器,然后把动态菜单和按钮权限存到 Redis。每次请求进来,过滤器解析 token、加载用户权限,再通过 Spring Security 的@PreAuthorize注解做接口级权限校验。
实际项目中,很多开发者只做了"接口级权限"(谁能调这个接口),而忘了"数据级权限"(同一个接口,社工只能看到自己网格的老人)。这个坑我在下一节专门讲,这里先埋个伏笔。
4.2 健康数据异常判定与告警推送的实现
健康数据接入有两种方式:一是智能设备通过 HTTP 接口直接上报,二是社工手动录入。我设计了一个统一的HealthRecordService来做数据接收和异常判定:
@Service public class HealthRecordService { private static final Map<String, HealthRule> RULE_MAP = new HashMap<>(); static { RULE_MAP.put("blood_pressure", new HealthRule(90, 140, 60, 90)); RULE_MAP.put("blood_sugar", new HealthRule(3.9, 6.1, null, null)); RULE_MAP.put("heart_rate", new HealthRule(60, 100, null, null)); } @Transactional(rollbackFor = Exception.class) public void handleHealthData(HealthRecord record) { // 1. 查老人档案,确认老人存在且状态正常 ElderlyInfo elderly = elderlyInfoMapper.selectById(record.getElderlyId()); if (elderly == null) { throw new ServiceException("老人档案不存在"); } // 2. 根据指标类型做异常判定 HealthRule rule = RULE_MAP.get(record.getRecordType()); boolean abnormal = judgeAbnormal(record, rule); record.setIsAbnormal(abnormal ? 1 : 0); // 3. 保存健康记录 healthRecordMapper.insert(record); // 4. 如果异常,生成告警记录并触发通知 if (abnormal) { generateAlarm(record, elderly); } } private boolean judgeAbnormal(HealthRecord record, HealthRule rule) { switch (record.getRecordType()) { case "blood_pressure": return record.getSystolicPressure() > rule.getMaxHigh() || record.getSystolicPressure() < rule.getMinHigh() || record.getDiastolicPressure() > rule.getMaxLow() || record.getDiastolicPressure() < rule.getMinLow(); case "blood_sugar": return record.getBloodSugarValue() > rule.getMaxHigh() || record.getBloodSugarValue() < rule.getMinHigh(); case "heart_rate": return record.getHeartRateValue() > rule.getMaxHigh() || record.getHeartRateValue() < rule.getMinHigh(); default: return false; } } }告警生成之后要处理两件事:一是写告警记录表,设置处理状态为"待确认";二是通过短信/公众号模板消息通知家属。这块我在做的时候学到的一个经验是:告警不是越快越好。如果老人血压 141 就立刻给家属发"血压偏高",家属一天能收到七八条,最后直接免疫了,真正严重的告警也被无视。更要命的是半夜给家属发"心率65正常"之类消息,就是骚扰。
所以我把告警分等级:数值偏离阈值但在安全区间内的,判为"观察提醒",只在系统内生成一条待回访记录;超过危险阈值的,才触发短信通知家属并转给社工处理。比如高压超过 170 才发短信,120~170 之间只是系统提醒。这个阈值规则我放在了数据库配置表里,方便业务人员调整,而不是写死在代码中。
4.3 服务工单闭环的完整实现
工单状态流转前面已经说清楚了,这里给一个核心代码片段。关键在于封装一个状态校验方法,让所有状态变更走同一入口。
@Service public class ServiceOrderService { @Autowired private ServiceOrderMapper serviceOrderMapper; private static final Map<Integer, Set<Integer>> TRANSITION_MAP = new HashMap<>(); static { TRANSITION_MAP.put(0, Set.of(1, 4)); // 待派单 -> 待服务/取消 TRANSITION_MAP.put(1, Set.of(2, 4, 5)); // 待服务 -> 服务中/取消/驳回 TRANSITION_MAP.put(2, Set.of(3)); // 服务中 -> 已完成 } @Transactional(rollbackFor = Exception.class) public void changeStatus(Long orderId, Integer targetStatus, Long operatorId) { ServiceOrder order = serviceOrderMapper.selectById(orderId); if (order == null) { throw new ServiceException("工单不存在"); } Set<Integer> allowed = TRANSITION_MAP.get(order.getStatus()); if (allowed == null || !allowed.contains(targetStatus)) { throw new ServiceException("非法状态流转: " + order.getStatus() + " -> " + targetStatus); } // 业务校验:取消要填原因,完成要校验预约时间、服务人等 order.setStatus(targetStatus); order.setUpdateTime(new Date()); serviceOrderMapper.updateById(order); } }这段代码看着简单,但它解决了最头大的问题:并发状态下重复提交导致的脏数据。比如服务员手速快点两下"完成服务",如果没有状态校验,数据可能被更新两次,虽然结果一样,但操作日志会多出一条,审计的时候说不清。
另外,我在创建工单的接口上做了防重处理。思路是:前端生成一个requestId(UUID),后端在 Redis 里以这个 requestId 为 key 设置过期时间 10 秒的锁,如果 key 已存在,直接拒绝请求,返回"请勿重复提交"。这个方案比数据库唯一索引更灵活,因为工单本身没有业务唯一键,用 requestId 做防重最干净。
5. 真正上线前容易踩的坑:数据权限、接口幂等与部署细节
这段内容全部来自真实上线过程中踩过的坑。很多系统在 Demo 阶段一切正常,一上生产就各种问题,根源往往不在功能逻辑,而在这几类非功能性设计上。
5.1 数据权限:为什么光有角色认证还不够
社区场景下,每个社工只管自己网格的老人。如果账号权限只做到"社工可以访问老人档案接口",那么任何一个社工登录后,都能通过循环遍历 ID 查看全街道的老人数据。这在功能上没错,在数据安全和业务规则上是严重缺陷。
我的解决思路:在用户表加一个area_code(网格编码),老人档案表也加area_code。查询列表时,在 MyBatis-Plus 查询构造器里强制追加数据权限条件:
public void appendDataScope(LambdaQueryWrapper<ElderlyInfo> wrapper, LoginUser loginUser) { if (loginUser.isAdmin()) { return; // 管理员不过滤 } if ("community_worker".equals(loginUser.getRoleCode())) { wrapper.eq(ElderlyInfo::getAreaCode, loginUser.getAreaCode()); } // 服务人员只看到自己被服务对象 if ("service_person".equals(loginUser.getRoleCode())) { wrapper.in(ElderlyInfo::getId, getServedElderlyIds(loginUser.getUserId())); } }这里有一个细节差点坑死人:服务人员权限有个特殊情况,工单创建人可以看全街道老人列表(因为要选服务对象),但只能看当前是"待派单"状态的工单。如果一刀切按 area_code 过滤,社工根本没法正常建单。所以数据权限要区分三个维度:菜单权限(能不能进)、接口权限(能不能调用)、数据范围(能看哪些行)。三者独立控制,才能灵活应对真实业务。
5.2 接口设计里的几个坑
第一个坑是时间字段的时区问题。服务器如果是 UTC 时区,数据库连接串没指定serverTimezone=Asia/Shanghai,查询出来的时间就会差 8 小时。健康数据差 8 小时意味着凌晨的告警记录被显示成上午,家属会误以为老人白天出事,直接打电话过来质问。解决方案:JDBC 连接串必须带上时区参数,应用层统一用LocalDateTime,前端展示时再格式化。
第二个坑是身份证号校验切忌只验长度。社区录入人员有时候会把 15 位老身份证号、身份证带 X 的都录进来,如果只按 18 位校验,一批老人都建档失败。我的做法是参照现行身份证编码规则写一个校验工具,兼容 15 位和 18 位,生日字段直接从身份证里提取,避免手工填错。18 位身份证最后一位是校验码,算法固定,网上有现成实现,别自己在纸上推,费时间还容易错。
第三个坑是导出功能导致服务器内存溢出。列表页+导出是标配,但直接用 POI 把几万条记录一次性查出来写入 Excel,老服务器的内存直接被打爆。我给导出功能加了一个限制:单次导出最多 5000 条,超过之后提示用户按条件筛选导出。同时用SXSSFWorkbook(流式处理),一边查一边写,内存占用从几百兆降到几十兆。这个优化做完,导出功能才算真正能用了。
5.3 打包部署与老社区网络环境的适配
部署环境通常是一台 2C4G 的云服务器或机房实体机,操作系统是 CentOS 7 这种老家伙。打包部署采用最直接的方案:Maven 打成可执行 JAR,配合 systemd 服务管理。JVM 参数我用的是一组保守配置:
java -Xms512m -Xmx1024m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m \ -jar smart-elderly-care.jar --spring.profiles.active=prod &这里要特别注意:MySQL 连接池、Redis 连接池的最大连接数不要按默认配。默认的 HikariCP 连接池最大连接数是 10,看起来够用,但社区场景会有集中录入的高峰(比如上午社工统一上传数据),连接会排队甚至超时。我把连接池最大设到 20,最小空闲 5 个,性能立刻改善。在 4G 内存的服务器上,这个值要配合 JVM 内存一起调,不能只追求大。
网络环境的适配也是坑。有的社区网络出口会有白名单策略,短信服务商的接口域名不一定开放,导致告警短信发不出去。我的建议是:提前与居委会 IT 确认好服务器出网策略和短信/公众号服务商的支持情况,并且做好公告集群发消息失败后的重试与记录,谁能看到错误信息,谁负责重试,都要在系统里留痕。
6. 从测试到上线:我在实际项目中沉淀的几个额外经验
系统开发完成只是第一步,真正让它跑起来、跑得好,靠的是上线前后的打磨。这部分我写几个不属于代码、但同样影响项目成败的经验。
第一,上线前一定要找真实社工试用,而不是自己拿测试数据自嗨。我当时的做法是找一个关系不错的社区,把系统部署到测试环境,让两位社工拿真实老人数据试了一周。结果是:她们一眼就发现了订单列表缺少"按时间倒序"排序,健康档案录入时缺少"最近一次测量时间"的快捷筛选,这些都是我坐在电脑前想象不出来的真实使用习惯。业务系统的交互细节,只有实际使用者能告诉你答案。
第二,迁移存量数据比开发系统更费时间。社区里有大量存量老人档案,纸质表格、Excel、政务系统导出的格式各不相同,身份证格式不统一、电话缺位、地址写法混乱。我当时安排了两周左右专门做数据清洗:写脚本批量校验、人工核对、按网格分批导入。数据质量决定系统初期体验,如果导入 5000 个老人,有 300 个身份证格式不对,社工一搜名字搜不到,信任感瞬间崩塌。
第三,预留一个扩展字段位。养老政策和社区服务方向变化很快,今天补贴类型有三种,明天就可能加一个新的。我在老人档案表里预留了ext_infoJSON 字段,系统级配置表留了config_key/config_value,这样政策调整时不用动表结构,改配置就能跟上。
第四,一定要有除草机制。智慧养老系统的核心数据是老人档案。老人去世、搬离辖区之后,status要由社工及时置为注销状态,而不是一直占用活动档案。我加了每月提醒功能:系统自动筛出一年没有健康记录、没有服务工单的老人,生成"活跃度预警"列表,让社工主动核实。这个功能对数据保鲜的帮助极大,也是这类系统区别于普通 CRUD 系统的价值所在。
最后再说一个理念层面的体会:社区智慧养老系统的难点,从来不是技术,而是对业务细节的捕捉和尊重。你要理解社工的一天是怎么度过的:她们早上要批量录入数据,中午要给老人打电话约服务,下午要上门回访,晚上可能还要处理一条健康告警。系统如果能把她们从反复抄写、到处翻数据里解放出来,这个 SpringBoot 项目就算真正做成了。技术再炫,最终都是为了一句话:让老人能得到更及时、更安心的照护。