☰
基于SpringBoot的银龄健康云服务平台设计、实现与部署
2026/10/8 20:24:09 网站建设 项目流程

1. 先想清楚为谁做:老年医疗网站的需求分析不能拍脑袋

做《基于 SpringBoot 的银龄健康云服务平台》这个题目的第一周,我其实挺兴奋的。SpringBoot 写后端、搭一套增删改查的管理系统,对我来说不算难事,真正难的是想明白一个问题:这个网站到底要给谁用,他们各自需要什么。

很多同学做毕业设计容易陷入一个误区——拿到题目先建工程、写接口,结果做到最后发现只是把教科书里的 CRUD 套了一层"老年""医疗"的皮。评审老师一问"你为什么这样设计",答不上来。我的建议是,花至少三天时间把需求盘清楚,后面所有开发都会顺很多。

1.1 老人、家属、医生三方诉求差异

老年医疗保健网站和普通的企业官网、电商系统有本质区别:它的使用对象不是一个单一群体,而是至少三类人。

第一类是老年人本人。他们的特点是操作能力弱、视力下降、对复杂界面有天然的抗拒。所以核心诉求就四个字:简单直接。他们要能快速看到自己的健康档案,能一键联系到家庭医生,能接到用药提醒。凡是需要三步以上才能完成的操作,对老年用户来说就是不可用。

第二类是家属。这才是系统真正的"活跃用户"。年轻人有 app 使用习惯,也最关心父母的健康数据。他们想看到的是:父母今天血压是否正常、是否按时吃了药、最近一次体检结果如何。所以在设计时,家属端的信息密度要大,趋势图表要直观,异常数据要醒目。

第三类是医生和平台管理员。医生需要高效地查看患者的历史病历、进行线上问诊、开具电子处方;管理员则关注用户管理、资讯发布、数据统计这些后台能力。

我把这三类诉求列了一张简单的对照表,贴在工位上做开发指引:

角色核心诉求对应功能模块
老年人操作简单、信息直达健康档案查看、一键呼叫医生、用药提醒
家属远程监护、异常感知健康数据共享、趋势图表、消息通知
医生高效问诊、有据可查患者列表、病历管理、在线答复
管理员平台运营、内容管理用户管理、资讯发布、数据报表

1.2 非功能性需求:医疗数据的合规与稳定要求

除了一堆功能点,医疗类系统还有几个容易被忽视的非功能性需求,这里单独拎出来说。

数据安全是最敏感的一环。健康档案属于个人敏感信息,按照隐私保护的思路,密码绝不能明文存储,血压、心率这类测量数据在展示给家属和医生之外的第三方时也要做脱敏。虽然毕业设计不涉及真实患者数据,但从一开始培养这种意识,对将来工作也有很大帮助。

其次是系统的稳定性要求。用药提醒这类功能依赖定时任务,如果服务频繁宕机,提醒就会失效,这在真实场景里可能造成严重后果。所以我在设计时给所有关键服务加了简单的失败告警日志,至少做到出了问题能快速定位。

另外还有一点:操作日志。谁在什么时候查看了某位老人的健康档案,这类记录在医疗系统里是审计必须。我用一张简单的操作日志表解决了这个问题,后面会细说。

2. 技术选型复盘:SpringBoot 3.x 还是 2.x,MyBatis-Plus 还是 JPA

题目要求用 SpringBoot,这一点没什么悬念。但真正动手前,我在版本选型和持久层框架上纠结了比较久,这里把我的取舍逻辑完整写出来,供同样做这个方向的同学参考。

2.1 框架选型的核心逻辑

先说 SpringBoot 版本。当前主流的稳定版本有 2.7.x 和 3.x 两个系列。3.x 是基于 Spring Framework 6 的,底层用 Jakarta EE,要求 JDK17 起步。如果你用的是 JDK8,那只能走 2.7.x;如果电脑上装的是 JDK17 或更高,建议直接用 SpringBoot 3.x。我最后选的是 SpringBoot 3.1.5 + JDK17,理由很简单:新项目没必要守旧,3.x 的启动性能和安全性都有提升,而且毕业设计周期内不容易遇到版本过期问题。

持久层框架我在 MyBatis-Plus 和 Spring Data JPA 之间反复对比过。JPA 的优点是实体关系映射自动程度高,CRUD 几乎不用写 SQL;缺点是复杂查询一旦涉及多表关联,要么写 JPQL,要么用 Specifications,学习曲线不小。MyBatis-Plus 则是在 MyBatis 基础上做了增强,单表 CRUD 可以用内置方法,复杂查询直接写 XML,调试 SQL 非常直观。

对这个项目来说,健康档案、预约记录、体检数据之间的关联查询非常频繁,而且我需要精确控制 SQL 来优化性能,所以 MyBatis-Plus 更合适。尤其它的分页插件,在列表页里能省很多事,后面会细讲。

2.2 后端分层架构与目录结构

SpringBoot 项目通常不强制分层,但做医疗这种业务逻辑复杂的系统,不分层后期维护会非常痛苦。我采用的还是经典的四层结构,并在实践中做了一点调整。

com.silverhealth ├── controller // 接口层,只做参数接收和结果封装 ├── service // 业务层,事务和核心逻辑都在这 ├── mapper // 数据访问层,MyBatis-Plus 的 Mapper 接口 ├── entity // 数据库实体类 ├── dto // 前端交互的数据传输对象 ├── vo // 视图对象,避免直接暴露实体类字段 ├── config // 配置类(安全配置、跨域配置、定时任务) ├── common // 通用返回结果、异常处理、工具类 └── job // 定时任务类

这套分层最大的好处是,controller 层可以做到非常薄。比如"提交血压测量记录"这个接口,controller 只负责调用 service 的一个方法,所有的参数校验、业务判断、日志记录都在 service 层完成。这样一旦出了问题,排查范围能快速缩小。

2.3 依赖清单与版本对照

这里把我最终使用的核心依赖整理成了一张表,顺便标注了用途,方便你直接照着配 pom.xml。

依赖版本用途说明
spring-boot-starter-web3.1.5构建 RESTful 接口
mybatis-plus-boot-starter3.5.5持久层增强,提供分页插件
mysql-connector-j8.1.0MySQL 8.0 驱动
spring-boot-starter-security3.1.5认证与权限控制
jjwt-api / impl0.11.5生成与校验 JWT Token
spring-boot-starter-data-redis3.1.5缓存热点数据、存储验证码
lombok1.18.30简化实体类代码
spring-boot-starter-validation3.1.5参数校验

这里有个小提醒:如果你的项目是基于 JDK8 的,SpringBoot 只能选 2.7.x,那么 mybatis-plus 也要选择对应的 3.5.3 之前的版本,否则会出现兼容问题。第一次做的时候我在这个坑里卡了大半天。

3. 核心功能模块的实现路线:健康档案、预约问诊、用药提醒一个都不能少

需求清楚、技术栈定了,接下来就是最花时间的功能开发阶段。我把整个平台拆成了六个核心模块,这里按照实现难度从低到高逐一说清楚设计思路和关键代码逻辑。

3.1 健康档案:一条主记录加 N 条随访记录

健康档案是整个平台的数据基石。我采用的是"主从表"设计:老年人基本信息放在 elder_info 表,每次的体检指标、随访记录放在 health_record 表,通过 elder_id 关联。

elder_info 表里除了姓名、年龄、性别、联系电话这些基础字段,还加了几个医疗系统特有的字段:紧急联系人、既往病史、过敏药物、血型、医保编号。这些字段在真实场景中都是救命的,做数据库设计时千万不要省略。

health_record 表则存储每次测量或体检的数据快照。字段包括血压收缩压/舒张压、心率、空腹血糖、体重、身高、记录时间,以及一个记录类型字段用来区分是"手动录入"还是"设备同步"。这个字段很重要,因为不同来源的数据可信度不同,医生查看时要能区分。

对应地,我写了一个根据 elder_id 查询最新健康记录的方法:

public HealthRecord getLatestRecord(Long elderId) { LambdaQueryWrapper<HealthRecord> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(HealthRecord::getElderId, elderId) .orderByDesc(HealthRecord::getRecordTime) .last("LIMIT 1"); return healthRecordMapper.selectOne(wrapper); }

MyBatis-Plus 的 LambdaQueryWrapper 在这种单表简单查询场景下非常好用,不需要写 XML,类型安全也能在编译期发现字段拼写错误。这一点是它比手写 SQL 占优势的地方。

3.2 在线预约与虚拟问诊:状态机的设计比接口更重要

预约挂号和在线问诊这两个模块,表面上看起来就是"新增一条预约记录""给问诊表插入一行数据",但如果你真这么写,后面一定会返工。因为预约这个动作天然是一个状态流转过程。

我设计的预约状态有五个:待确认、已确认、已完成、已取消、已过期。对应的流转规则是:

  • 用户提交预约后,初始状态是"待确认"
  • 医生确认接诊,状态变为"已确认"
  • 预约时间到达且医生填写了问诊结论,状态变为"已完成"
  • 用户或医生取消预约,状态变为"已取消"
  • 预约时间已过但医生未确认,定时任务将状态自动置为"已过期"

为了保证状态不混乱,我在 service 层封装了一个专门的状态流转方法,所有状态变更必须走这个方法,禁止在 controller 层直接调用 update 方法修改状态。这样做的好处是,后续加"医生取消""用户爽约"之类的分支逻辑时,只需要在一个地方改。

这里贴一段核心的状态校验逻辑:

public boolean changeStatus(Long appointmentId, AppointmentStatus target) { Appointment appointment = getById(appointmentId); Set<AppointmentStatus> allowed = TRANSITIONS.get(appointment.getStatus()); if (allowed == null || !allowed.contains(target)) { throw new BusinessException("非法的状态流转: " + appointment.getStatus() + " -> " + target); } appointment.setStatus(target); return updateById(appointment); }

把状态流转关系放在一个 Map 常量里维护,可读性比一堆 if-else 好太多。

3.3 用药提醒:定时任务加站内信的实际组合

用药提醒是我觉得这个系统里最有"养老"味道的功能。老年人最大的痛点之一就是忘记吃药,尤其是有高血压、糖尿病这类需要长期服药的人群。

技术实现上,我用了 SpringBoot 自带的 @Scheduled 注解。每天凌晨跑一次任务,扫描 reminder_rule 表里所有"提醒时间等于今天"的记录,然后向关联的用户生成一条站内信通知,同时给绑定了微信的家属发送一条提醒消息(微信推送需要额外对接,这里先预留接口)。

核心代码如下:

@Component public class MedicineReminderJob { @Scheduled(cron = "0 0 8 * * ?") public void sendDailyReminders() { List<ReminderRule> rules = reminderRuleMapper.selectList( new LambdaQueryWrapper<ReminderRule>() .eq(ReminderRule::getEnabled, 1) .eq(ReminderRule::getRemindTime, LocalDate.now()) ); for (ReminderRule rule : rules) { Notification notification = new Notification(); notification.setUserId(rule.getElderId()); notification.setContent("您今天需要服用: " + rule.getMedicineName()); notificationMapper.insert(notification); } } }

这里补充一个我在开发中发现的细节:@Scheduled 默认是单线程执行的,如果定时任务方法里有耗时操作(比如调用第三方短信接口),会阻塞后续任务。我建议在启动类或者配置类里搞一个线程池:

@Configuration public class ScheduledConfig implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5)); } }

这算是一个潜在的坑,我后面会在踩坑记录里单独展开。

4. 数据库设计的三个关键决策:分表、冗余与状态字段

很多做 SpringBoot 项目的同学有一个通病:代码写得很熟练,数据库表设计却非常随意。实际上对于医疗保健这种存在大量关联查询的系统来说,表结构的设计质量直接决定了系统能跑多快、能撑多大。这里我拿出三个当时纠结最久的决策来复盘。

4.1 核心表结构拆解

整个系统我建了十几张表,其中有五张是最核心的骨架:

表名用途关键字段
elder_info老年人基础档案elder_id(主键), name, age, phone, emergency_contact, medical_history
family_binding家属绑定关系id, elder_id, family_id, relation, bind_time
health_record健康数据记录id, elder_id, systolic, diastolic, heart_rate, blood_glucose, record_time
appointment预约挂号id, elder_id, doctor_id, appoint_time, status, description
notification消息通知id, user_id, content, type, is_read, create_time

family_binding 这张表值得多说一句。老人和家属之间是多对多关系,一个老人可以绑定多个子女,一个子女也可能同时照料两位老人。所以不能直接在 elder_info 表里加一个 family_id 字段,而是单独建一张中间表。在 MyBatis-Plus 里做多对多查询时,我用了两次查询而不是一次大 JOIN,这样 SQL 更简单,性能在数据量不大的情况下也完全够用。

4.2 状态字段用 int 还是 varchar

这是个看起来很基础、实际上很影响系统质量的问题。很多人图省事,直接在表里用 varchar 存"已完成""已取消"这类中文描述,看着直观,但有两个麻烦:一是业务层判断时只能用字符串比对,容易因为多一个空格、改了一个字而出问题;二是数据库里存中文占用的空间更大,索引效率也低。

我统一采用 int 类型配合常量类来管理状态值。比如预约状态:

public class AppointmentStatus { public static final int PENDING = 0; public static final int CONFIRMED = 1; public static final int COMPLETED = 2; public static final int CANCELLED = 3; public static final int EXPIRED = 4; }

实体类里再用 @EnumValue 注解或者直接在 VO 转换时把 int 映射成前端需要的字符串。这样数据库层面干净,业务代码里可读性也高。这个习惯我是从一个工作多年的学长那学来的,非常推荐。

4.3 分页查询性能优化的一个实际案例

健康记录列表页,家属端要按时间分页查看老人的血压、血糖数据。最开始我用 MyBatis-Plus 的分页插件直接 selectPage,本地测试几千条数据没问题。但后来造了十万条测试数据想看看极限,发现列表页响应时间到了两秒以上。

用日志一分析,发现耗时的部分不在数据查询,而在 COUNT 语句——MyBatis-Plus 默认会执行一条SELECT COUNT(*) FROM health_record WHERE elder_id = ?。如果 WHERE 条件里还有多个筛选字段,这个计数查询会走全表扫描。

我的优化方案是给 elder_id 和 record_time 建了一个联合索引:

ALTER TABLE health_record ADD INDEX idx_elder_time (elder_id, record_time);

加了索引之后,十万条数据量下列表页响应时间降到了 200 毫秒以内。对毕业设计来说,这个量级的优化已经足够,但我把思路放在这里:如果数据量继续涨到百万级,就要考虑按月份做分表,或者引入 ClickHouse 这类分析型数据库了。

5. 权限与安全:医疗场景下的一点点偏执

医疗数据是敏感数据,这一点怎么强调都不过分。我从一开始就决定,认证和权限部分不能糊弄,否则整个系统的专业度会大打折扣。

5.1 基于 Spring Security 加 JWT 的多角色登录

这个平台的登录用户分四种:老年人、家属、医生、管理员。我选择了 Spring Security 加 JWT 的方案。JWT 的好处是无状态,适合前后端分离的架构;Spring Security 则负责提供标准的认证流程和过滤链。

实现思路是:用户登录成功后,后端根据用户 ID 和角色生成一个 Token,有效期设为一周(老年用户不会频繁登录,太短会带来困扰)。前端把 Token 存在本地存储里,每次请求在 Authorization 头带上。后端通过一个 OncePerRequestFilter 过滤器,在请求进入 controller 之前完成 Token 的解析和用户身份的注入。

核心过滤器的逻辑大致是:

public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String token = getTokenFromRequest(request); if (StringUtils.hasText(token) && jwtUtil.validateToken(token)) { Integer userId = jwtUtil.getUserId(token); String role = jwtUtil.getRole(token); UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken( userId, null, List.of(new SimpleGrantedAuthority("ROLE_" + role))); SecurityContextHolder.getContext().setAuthentication(auth); } chain.doFilter(request, response); } }

这里有一个关键点:ROLE_ 前缀必须加上,否则 Spring Security 的方法级权限注解@PreAuthorize("hasRole('DOCTOR')")会匹配不上,我第一次踩这个坑时找了很久。

5.2 数据权限:老人只能看到自己的病历

角色权限只是第一层,更麻烦的是同一角色内部的数据权限。再举个例子:一个医生可以查看所有分配给自己的患者,但不能看其他医生的患者;一个家属只能看自己绑定的老人,不能查别人的。这两条规则不处理好,系统就等于裸奔。

我在实现时,所有涉及健康数据查询的业务方法里都强制带上了权限过滤条件。比如家属查询老人健康记录时:

public List<HealthRecordVO> queryElderRecords(Long currentUserId, Long elderId) { // 先确认 bind 关系 FamilyBinding binding = bindingMapper.selectOne( new LambdaQueryWrapper<FamilyBinding>() .eq(FamilyBinding::getFamilyId, currentUserId) .eq(FamilyBinding::getElderId, elderId)); if (binding == null) { throw new BusinessException("无权查看该老人的健康数据"); } // 再执行查询 ... }

这种在 service 层手动校验的方式虽然比注解式权限繁琐,但是胜在一目了然,很难出现绕过校验的漏洞。

5.3 密码加密与敏感数据脱敏

密码存储我用的是 BCrypt 加密,这是 Spring Security 自带的能力。BCrypt 加盐的策略是内置的,每次 hash 出来的结果都不一样,但用校验方法验证时又能匹配,不需要额外存储盐值。千万不要用 MD5 存密码,那跟明文没有本质区别。

脱敏方面,老年人在家属端的列表页展示时,手机号中间四位用星号替代。这个逻辑我放在 VO 转换时处理,而不是在数据库层做,因为医院内部管理端有时需要看完整号码,但对外展示必须脱敏。把脱敏逻辑放在不同的 DTO/VO 里,语义更清晰。

6. 开发过程中的真实踩坑记录

这部分是我最想写的。三个月开发下来,真正让我成长的不是那些顺利的功能实现,而是一个个排查很久的怪问题。我把其中最典型的四个写出来,希望你能绕开。

6.1 定时任务线程阻塞导致用药提醒延迟

现象:用药提醒功能上线后发现,某些用户的提醒信会在中午十二点才收到,但配置的是早上八点发送。查日志发现,任务确实在八点启动了,但执行到一半卡住了。

原因:@Scheduled 默认使用单线程调度器,如果某个定时任务方法里调用了外部接口(比如短信服务),而外部接口响应超时(默认可能几十秒),就会把整个调度线程堵住。后面排队的任务全部等待,导致提醒延迟。

解决:前面已经说过,注入一个线程池给调度器。同时把所有外部调用都包一层超时控制。这是一个非常典型的真实场景问题,评审老师如果问到定时任务,说这个绝对加分。

6.2 MyBatis-Plus 分页插件的一个隐藏坑

现象:列表页接口总是返回 total 等于 0,但数据明明有几十条。

原因:MyBatis-Plus 的分页插件要求在项目中显式配置一个 PaginationInnerInterceptor。如果你只引了依赖而忘了注册这个拦截器,分页插件不会生效,selectPage 查出来的 total 就是 0,数据也不会真正分页。

解决:在配置类里手动加:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

注意:SpringBoot 3.x 和 MyBatis-Plus 3.5.x 配合时,分页插件的 DbType 必须是 MYSQL,否则 LIMIT 语句的方言可能生成错误。

6.3 文件上传之后头像不显示

现象:老年人和医生都可以上传头像,本地上传成功后,img 标签却访问不到图片,报 404。

原因:SpringBoot 默认的静态资源映射只覆盖 classpath 下的 static 目录。上传到本机磁盘的图片不在这个范围内,必须额外配置资源映射。

解决:在配置里加一个映射规则:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceHandler("file:" + uploadPath + "/"); } }

还要注意一个坑:如果 uploadPath 写的是相对路径,部署到服务器后很可能因为当前工作目录不同而找不到文件。建议在 application.yml 里用绝对路径配置,这个教训我是在服务器上部署后白屏两小时才记住的。

6.4 实体类字段映射失败:下划线与驼峰的约定

现象:elderInfo 表的 emergency_contact 字段在实体类里写成 emergencyContact,查询出来一直是 null。

原因:MyBatis-Plus 默认开启了驼峰映射(map-underscore-to-camel-case),但前提是数据库字段名是下划线风格,而实体类字段是驼峰风格。按理说 emergency_contact 映射到 emergencyContact 是自动的。直到我发现,有一张表的字段命名不规范,混用了大小写,导致映射不到。

解决:严格统一所有表的字段命名规范,全部采用小写下划线风格。这也是为什么我在数据库设计阶段就要定好所有表结构和字段名,避免中途返工。

7. 从毕业设计到真实落地:部署、演示与后续演进

功能开发完只算完成了百分之七十,部署上线和演示准备往往才是决定最终能否顺利过关的关键环节。这一节把我在最后阶段做的事和思考写全。

7.1 本地跑通到云服务器部署

我是在一台 2C4G 的云服务器上部署的。环境配置步骤如下:

  1. 安装 JDK17 和 MySQL 8.0,注意 MySQL 的字符集一定要设置为 utf8mb4,否则中文可能有乱码风险
  2. 用 Maven 打包:mvn clean package -DskipTests,生成可执行的 jar 包
  3. 把 jar 包上传到服务器,用 nohup 启动:
nohup java -jar silver-health-server.jar --spring.profiles.active=prod > app.log 2>&1 &
  1. 在安全组里放行 8080 端口,再把域名解析过来(如果用了 nginx 做反向代理,记得在 nginx 里配好 location 转发)

这里有一个容易忽略的点:application-prod.yml 里的数据库连接地址、文件上传路径、JWT 密钥都要单独配置,不要把本地开发配置直接带到生产环境。尤其是密钥,写成硬编码会有安全隐患,我是在启动参数里通过环境变量注入的。

7.2 演示版本的准备清单

答辩演示时最怕的就是现场翻车。我提前做了几件事:

  • 准备了一套完整的演示数据。这个演示库里有三四个老人、两位医生、十几条健康记录、几条待确认的预约,确保每个界面打开都有内容,不会出现空白列表
  • 提前把所有账号密码记好,切换角色时能快速登录。JWT 有效期设置得足够长,避免演示中途 Token 过期
  • 留了一条清晰的"故事线":从家属登录查看父母健康数据异常,到发起线上咨询,到医生回复并修改用药提醒,整个过程串成一条业务闭环

演示的价值不在于展示所有功能,而在于让评委在五分钟内理解你的系统解决了什么问题。

7.3 这个系统还能往哪些方向扩展

做完一个版本之后回头看,其实有很多值得继续深化的方向。我列几个个人觉得很有价值、也符合智慧养老趋势的扩展点,给将来想继续做这个方向的同学一点参考。

一是接入智能硬件设备。像血压计、血糖仪、智能手环这类设备的数据如果能自动同步到平台,健康档案就从"手动记录"升级成"自动采集",这会让系统价值上一个台阶。

二是语音交互。老年用户打字困难,如果能支持简单的语音指令(比如"今天要吃什么药"),会大大降低使用门槛。这个方向可以用现有的语音识别 API 做,技术难度并不高。

三是基于健康数据的异常预警。血压数据连续三天偏高时自动通知家属和医生,这类规则引擎的实现,在 SpringBoot 里用定时任务加条件判断就能做初版,效果却非常好。

我自己的体会是,毕业设计与其说是为了交一份作业,不如说是完整经历一次"需求分析、架构设计、编码实现、部署运维"的全流程。做完这个项目之后,我对 SpringBoot 的理解不再是停留在"会用注解写接口"的层面,而是真正理解了为什么分层、为什么要控制状态流转、为什么数据库设计要先于代码动手。这种体感,是刷多少面试题都换不来的。希望这篇复盘能帮到正在做类似题目的你,少踩几个我踩过的坑。

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

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

立即咨询