毕业设计实战:SpringBoot老年人体检管理系统,从需求到部署完整指南
当初做老年人体检管理系统时,我在“体检报告上传与预览”功能上卡了整整一周——一开始把体检报告存数据库,结果用户上传100页PDF直接崩了,导师看了直摇头😫 后来踩了无数坑,终于总结出这套完整开发流程。今天就把老年人体检管理系统的实战经验全部分享出来,宝子们跟着做,毕设稳过!
一、需求分析别跑偏!先搞懂“谁体检,谁管理”
最开始我以为做个简单的体检预约就行,结果导师说“要考虑老年人的特殊需求,要有健康档案管理、体检提醒、在线咨询”。后来才明白,老年人体检系统的核心是 “老年人预约-医生管理-家属查看” 的多角色协同,必须抓住这三个群体的核心需求。
1. 核心用户 & 核心功能(踩坑后总结版)
老年人体检管理系统有四类核心用户:老年人(体检者)、家属(协作者)、医生/管理员(管理者)、系统管理员。别把“社区管理员”、“体检中心”都加进去!我当初加了,权限体系变得极其复杂,最后简化成四级才顺畅。
-
老年人端(核心用户,必须做的功能):
- 个人信息管理:维护姓名、身份证号、联系方式、出生日期、住址等基本信息。
- 体检项目管理:这是核心中的核心!
- 浏览各类体检项目(基础体检、专项检查等)。
- 查看项目详情(包含哪些检查、注意事项)。
- 在线预约体检(选择时间、地点)。
- 我的体检记录:
- 查看预约记录及审核状态。
- 查看体检提醒(时间、地点、注意事项)。
- 下载体检报告(PDF格式)。
- 健康社区:
- 浏览健康常识、疾病预防知识。
- 参与论坛讨论。
- 在线咨询医生。
-
家属端(协作者,重要功能):
- 老人管理:绑定家中老人账号(需老人授权)。
- 体检提醒:接收老人体检提醒,可设置二次提醒。
- 报告查看:查看老人体检报告(有权限控制)。
- 健康监测:查看老人体检历史趋势。
-
医生/管理员端(管理者,核心功能):
- 预约审核:审核老年人的体检预约。
- 体检提醒管理:为老年人设置体检提醒。
- 报告上传:上传体检报告(支持PDF、图片)。
- 健康咨询:回复老年人的在线咨询。
- 健康数据统计:统计各项体检指标。
-
系统管理员端:
- 用户管理:管理所有用户账号。
- 内容管理:管理健康常识、疾病预防等文章。
- 论坛管理:审核论坛帖子。
- 数据备份:定期备份系统数据。
2. 需求分析避坑指南(血泪教训!)
- 一定要考虑老年人操作习惯:
- 字体要大!至少16px以上。
- 按钮要大!方便点击。
- 流程要简单!最多3步完成一个操作。
- 有语音提示更好(这个可以不做,但可以提出来作为扩展)。
- 数据隐私要重视:
- 体检报告只能本人和授权家属查看。
- 敏感信息(身份证号)要脱敏显示。
- 操作日志要完整记录。
- 写清楚约束条件:
- “同一项目同一天只能预约一次”
- “体检报告只能上传PDF或图片格式”
- “咨询问题24小时内必须回复”
- “体检前3天、1天自动发送提醒”
3. 可行性分析(三句话说清楚)
- 技术可行性:SpringBoot + MySQL + Vue,成熟稳定。体检报告预览可以用PDF.js,不需要复杂的文档转换。
- 经济可行性:所有工具免费,老年人可以用子女的智能手机操作,硬件成本低。
- 操作可行性:界面针对老年人优化,家属可以协助操作,医生有专业后台。
二、技术选型:SpringBoot真香!
当初我看别人用传统的SSH,配置复杂。后来选择了 SpringBoot 2.7 + MyBatis-Plus + Vue 2 + Element UI + Redis ,开发效率提升不止一倍!
技术栈详解与避坑
| 技术 | 选择理由 | 避坑提醒 |
|---|---|---|
| SpringBoot 2.7.x | 自动配置,快速启动,适合快速开发。 | 别用3.x,生态还不够成熟。 |
| MyBatis-Plus | 强大的CRUD操作,代码生成器好用。 | 学会用它的@TableLogic实现逻辑删除。 |
| Vue 2 + Element UI | 组件丰富,做管理后台很快。 | Element UI的字体默认太小,要全局调大。 |
| MySQL 8.0 | JSON字段支持,存体检项目配置方便。 | 一定用utf8mb4字符集,支持Emoji表情。 |
| Redis | 缓存体检项目列表,提高访问速度。 | 配置合理的过期时间,避免内存占用过高。 |
| PDF.js | 前端PDF预览,不需要后端转换。 | 注意文件大小限制,大文件要分页加载。 |
开发环境一步到位
# application.yml核心配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/old_health?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4
username: root
password: 123456
servlet:
multipart:
max-file-size: 50MB # 体检报告可能比较大
max-request-size: 50MB
redis:
host: localhost
port: 6379
database: 0
# MyBatis-Plus配置
mybatis-plus:
global-config:
db-config:
logic-delete-field: delete_flag # 全局逻辑删除字段
logic-delete-value: 1 # 逻辑已删除值
logic-not-delete-value: 0 # 逻辑未删除值
三、数据库设计:体检报告存储是关键!
我当初的坑:把体检报告存数据库的BLOB字段,结果查询慢得要死。后来改存文件服务器路径,只在数据库存路径和元数据。
核心表结构设计(重点!)
-- 老年人用户表(核心表)
CREATE TABLE `old_user` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '账号(手机号)',
`password` varchar(100) NOT NULL COMMENT '密码(MD5加密)',
`real_name` varchar(50) NOT NULL COMMENT '真实姓名',
`id_card` varchar(18) NOT NULL COMMENT '身份证号',
`phone` varchar(20) NOT NULL COMMENT '手机号',
`birth_date` date NOT NULL COMMENT '出生日期',
`age` int GENERATED ALWAYS AS (TIMESTAMPDIFF(YEAR, birth_date, CURDATE())) VIRTUAL COMMENT '计算年龄',
`gender` tinyint DEFAULT 1 COMMENT '1男,2女',
`address` varchar(200) COMMENT '住址',
`emergency_contact` varchar(50) COMMENT '紧急联系人',
`emergency_phone` varchar(20) COMMENT '紧急联系电话',
`health_status` tinyint DEFAULT 1 COMMENT '健康状况:1良好,2一般,3较差',
`family_member_ids` json COMMENT '绑定的家属ID列表',
`delete_flag` tinyint DEFAULT 0 COMMENT '0正常,1已删除',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_phone` (`phone`),
UNIQUE KEY `uk_idcard` (`id_card`),
KEY `idx_birthdate` (`birth_date`) -- 按出生日期查询
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='老年人用户表';
-- 体检项目表
CREATE TABLE `health_check_item` (
`id` int NOT NULL AUTO_INCREMENT,
`item_code` varchar(20) NOT NULL COMMENT '项目编码',
`item_name` varchar(100) NOT NULL COMMENT '项目名称',
`item_type` tinyint DEFAULT 1 COMMENT '1基础体检,2专项检查,3慢性病筛查',
`description` text COMMENT '项目描述',
`suitable_age` varchar(50) COMMENT '适宜年龄范围',
`frequency` varchar(50) COMMENT '建议频率',
`price` decimal(10,2) DEFAULT 0.00 COMMENT '参考价格',
`cover_image` varchar(200) COMMENT '封面图片',
`status` tinyint DEFAULT 1 COMMENT '1启用,0停用',
`sort_order` int DEFAULT 0 COMMENT '排序',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_item_code` (`item_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='体检项目表';
-- 体检预约表
CREATE TABLE `health_appointment` (
`id` int NOT NULL AUTO_INCREMENT,
`appointment_no` varchar(30) NOT NULL COMMENT '预约号:YYMMDD+6位随机',
`user_id` int NOT NULL COMMENT '用户ID',
`item_id` int NOT NULL COMMENT '体检项目ID',
`appointment_date` date NOT NULL COMMENT '预约日期',
`time_slot` varchar(20) COMMENT '时间段:上午/下午',
`hospital_name` varchar(100) NOT NULL COMMENT '体检机构',
`address` varchar(200) COMMENT '详细地址',
`contact_phone` varchar(20) COMMENT '联系电话',
`status` tinyint DEFAULT 1 COMMENT '1待审核,2已预约,3已完成,4已取消',
`audit_remark` varchar(500) COMMENT '审核意见',
`cancel_reason` varchar(500) COMMENT '取消原因',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_appointment_no` (`appointment_no`),
KEY `idx_user_date` (`user_id`, `appointment_date`),
KEY `idx_status` (`status`),
CONSTRAINT `fk_appointment_user` FOREIGN KEY (`user_id`) REFERENCES `old_user` (`id`),
CONSTRAINT `fk_appointment_item` FOREIGN KEY (`item_id`) REFERENCES `health_check_item` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='体检预约表';
-- 体检报告表(重要!)
CREATE TABLE `health_report` (
`id` int NOT NULL AUTO_INCREMENT,
`report_no` varchar(30) NOT NULL COMMENT '报告编号',
`appointment_id` int NOT NULL COMMENT '关联的预约',
`user_id` int NOT NULL COMMENT '用户ID',
`report_date` date NOT NULL COMMENT '体检日期',
`hospital_name` varchar(100) NOT NULL COMMENT '体检机构',
`doctor_name` varchar(50) COMMENT '医生姓名',
`summary` text COMMENT '体检总结',
`suggestion` text COMMENT '健康建议',
`file_path` varchar(500) NOT NULL COMMENT '报告文件路径',
`file_size` bigint DEFAULT 0 COMMENT '文件大小(字节)',
`file_type` varchar(20) DEFAULT 'pdf' COMMENT '文件类型',
`view_count` int DEFAULT 0 COMMENT '查看次数',
`is_abnormal` tinyint DEFAULT 0 COMMENT '0正常,1异常',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_report_no` (`report_no`),
KEY `idx_user_date` (`user_id`, `report_date`),
KEY `idx_appointment` (`appointment_id`),
CONSTRAINT `fk_report_appointment` FOREIGN KEY (`appointment_id`) REFERENCES `health_appointment` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='体检报告表';
-- 体检提醒表
CREATE TABLE `health_reminder` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`reminder_type` tinyint DEFAULT 1 COMMENT '1体检提醒,2用药提醒,3复诊提醒',
`title` varchar(100) NOT NULL,
`content` text NOT NULL,
`remind_time` datetime NOT NULL COMMENT '提醒时间',
`remind_days` int DEFAULT 0 COMMENT '提前几天提醒',
`status` tinyint DEFAULT 1 COMMENT '1待提醒,2已提醒,3已处理',
`is_repeated` tinyint DEFAULT 0 COMMENT '0单次,1重复',
`repeat_rule` varchar(100) COMMENT '重复规则',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_user_time` (`user_id`, `remind_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='体检提醒表';
设计亮点:
- 虚拟列计算年龄:MySQL 5.7+支持生成列,自动计算年龄,不用程序算。
- JSON字段存储家属:
family_member_ids用JSON存储绑定的家属ID,查询方便。 - 报告文件分离存储:文件存服务器,数据库只存路径,查询快。
- 多重索引优化:常用查询字段都加了索引。
四、功能实现:抓住老年人体检的特殊需求
1. 老年人端:体检预约(大字体大按钮)
关键逻辑:年龄适配推荐、时间冲突检查、一键预约。
前端实现要点(Vue + 大字体CSS):
<template>
<!-- 专门为老年人设计的体检预约页面 -->
<div class="old-people-page">
<!-- 超大标题 -->
<h1 class="big-title">体检预约</h1>
<!-- 体检项目推荐(根据年龄自动推荐) -->
<div class="recommend-section">
<h2 class="section-title">为您推荐的体检项目</h2>
<div class="item-list">
<div v-for="item in recommendItems" :key="item.id"
class="item-card big-card">
<div class="item-image">
<img :src="item.coverImage" alt="">
</div>
<div class="item-info">
<h3 class="item-name">{{ item.itemName }}</h3>
<p class="item-desc">{{ item.description }}</p>
<div class="item-tags">
<span class="tag">适合{{ item.suitableAge }}岁</span>
<span class="tag">{{ item.frequency }}</span>
</div>
<div class="item-actions">
<button class="big-btn detail-btn" @click="viewDetail(item)">
查看详情
</button>
<button class="big-btn primary-btn" @click="startAppointment(item)">
立即预约
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 一键预约按钮(特别大,特别显眼) -->
<div class="quick-action">
<button class="super-big-btn" @click="quickAppointment">
<i class="icon-phone"></i>
<span>一键预约体检</span>
</button>
<p class="tip-text">子女可协助操作</p>
</div>
</div>
</template>
<style scoped>
/* 专门为老年人设计的样式 */
.old-people-page {
font-size: 18px; /* 基础字体放大 */
line-height: 1.8;
}
.big-title {
font-size: 32px !important;
font-weight: bold;
color: #333;
margin-bottom: 30px;
}
.big-card {
padding: 25px;
margin-bottom: 20px;
border-radius: 15px;
background: #fff;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.big-btn {
padding: 15px 30px !important;
font-size: 18px !important;
border-radius: 10px;
margin: 10px 5px;
}
.super-big-btn {
display: block;
width: 90%;
max-width: 400px;
margin: 40px auto;
padding: 25px !important;
font-size: 24px !important;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 20px;
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
.super-big-btn:hover {
transform: translateY(-3px);
box-shadow: 0 15px 30px rgba(102, 126, 234, 0.4);
}
/* 语音朗读按钮(辅助功能) */
.speech-btn {
position: fixed;
right: 20px;
bottom: 20px;
width: 60px;
height: 60px;
border-radius: 50%;
background: #4CAF50;
color: white;
border: none;
font-size: 24px;
z-index: 1000;
}
</style>
<script>
export default {
data() {
return {
userAge: 65, // 从用户信息获取
recommendItems: []
}
},
mounted() {
this.loadRecommendItems();
},
methods: {
async loadRecommendItems() {
// 根据年龄获取推荐项目
const res = await this.$api.health.getRecommendItems({
age: this.userAge,
healthStatus: this.userHealthStatus
});
this.recommendItems = res.data;
},
startAppointment(item) {
// 检查今天是否已预约过
this.checkTodayAppointment(item.id).then(canAppoint => {
if (canAppoint) {
this.$router.push({
path: '/appointment/form',
query: { itemId: item.id }
});
} else {
this.$message({
type: 'warning',
message: '您今天已预约过体检,请勿重复预约',
duration: 5000 // 提示显示5秒,给老年人足够时间看
});
}
});
},
// 一键预约:推荐最合适的项目,选择最近的时间
async quickAppointment() {
try {
// 1. 获取最适合的项目
const bestItem = await this.$api.health.getBestItem(this.userAge);
// 2. 获取最近可预约的时间
const nextDate = await this.$api.appointment.getNextAvailableDate();
// 3. 自动填写表单
this.$router.push({
path: '/appointment/quick',
query: {
itemId: bestItem.id,
date: nextDate
}
});
} catch (error) {
this.$message.error('一键预约失败,请稍后重试');
}
},
// 语音朗读页面内容(辅助功能)
speakContent() {
if ('speechSynthesis' in window) {
const speech = new SpeechSynthesisUtterance();
speech.text = document.querySelector('.old-people-page').innerText;
speech.rate = 0.8; // 语速调慢
speech.pitch = 1;
speech.volume = 1;
window.speechSynthesis.speak(speech);
}
}
}
}
</script>
后端关键代码(年龄适配推荐):
@Service
public class HealthCheckItemServiceImpl implements HealthCheckItemService {
@Autowired
private HealthCheckItemMapper itemMapper;
@Override
public List<HealthCheckItemVO> getRecommendItems(Integer age, Integer healthStatus) {
// 1. 基础必检项目(所有年龄都需要)
LambdaQueryWrapper<HealthCheckItem> baseWrapper = new LambdaQueryWrapper<>();
baseWrapper.eq(HealthCheckItem::getItemType, 1) // 基础体检
.eq(HealthCheckItem::getStatus, 1)
.orderByAsc(HealthCheckItem::getSortOrder);
List<HealthCheckItem> baseItems = this.list(baseWrapper);
List<HealthCheckItemVO> result = baseItems.stream()
.map(this::convertToVO)
.collect(Collectors.toList());
// 2. 根据年龄推荐专项检查
if (age >= 60) {
// 60岁以上推荐:心脑血管、骨密度、眼底检查等
LambdaQueryWrapper<HealthCheckItem> ageWrapper = new LambdaQueryWrapper<>();
ageWrapper.like(HealthCheckItem::getSuitableAge, "60以上")
.or().like(HealthCheckItem::getSuitableAge, "老年人")
.eq(HealthCheckItem::getStatus, 1)
.orderByAsc(HealthCheckItem::getSortOrder);
List<HealthCheckItem> ageItems = this.list(ageWrapper);
ageItems.stream()
.map(this::convertToVO)
.forEach(result::add);
}
// 3. 根据健康状况推荐
if (healthStatus != null && healthStatus == 3) {
// 健康状况较差的推荐慢性病筛查
LambdaQueryWrapper<HealthCheckItem> healthWrapper = new LambdaQueryWrapper<>();
healthWrapper.eq(HealthCheckItem::getItemType, 3) // 慢性病筛查
.eq(HealthCheckItem::getStatus, 1)
.orderByAsc(HealthCheckItem::getSortOrder);
List<HealthCheckItem> healthItems = this.list(healthWrapper);
healthItems.stream()
.map(this::convertToVO)
.forEach(result::add);
}
return result;
}
@Override
public HealthCheckItem getBestItem(Integer age) {
// 智能推荐最适合的体检套餐
if (age >= 70) {
return this.lambdaQuery()
.eq(HealthCheckItem::getItemCode, "ELDERLY_FULL")
.one();
} else if (age >= 60) {
return this.lambdaQuery()
.eq(HealthCheckItem::getItemCode, "ELDERLY_BASIC")
.one();
} else {
return this.lambdaQuery()
.eq(HealthCheckItem::getItemCode, "GENERAL_FULL")
.one();
}
}
}
2. 体检报告上传与预览(核心功能)
关键逻辑:文件格式校验、PDF预览、权限控制。
报告上传组件:
<template>
<div class="report-upload">
<!-- 医生端:上传报告 -->
<div v-if="userRole === 'DOCTOR'" class="upload-section">
<el-upload
class="upload-demo"
drag
action="/api/report/upload"
:data="{ appointmentId: appointmentId }"
:before-upload="beforeUpload"
:on-success="handleSuccess"
:limit="1"
accept=".pdf,.jpg,.jpeg,.png"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">
将体检报告拖到此处,或<em>点击上传</em>
</div>
<div class="el-upload__tip" slot="tip">
只能上传PDF/JPG/PNG文件,且不超过50MB
</div>
</el-upload>
<!-- 报告信息填写 -->
<div v-if="fileUploaded" class="report-info">
<h3>填写报告信息</h3>
<el-form :model="reportForm" label-width="100px">
<el-form-item label="体检总结">
<el-input
type="textarea"
v-model="reportForm.summary"
:rows="4"
placeholder="填写体检总体情况"
/>
</el-form-item>
<el-form-item label="健康建议">
<el-input
type="textarea"
v-model="reportForm.suggestion"
:rows="4"
placeholder="填写健康建议和注意事项"
/>
</el-form-item>
<el-form-item label="是否异常">
<el-radio-group v-model="reportForm.isAbnormal">
<el-radio :label="0">正常</el-radio>
<el-radio :label="1">异常</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveReportInfo">
保存报告
</el-button>
</el-form-item>
</el-form>
</div>
</div>
<!-- 用户端:查看报告 -->
<div v-else class="view-section">
<div class="report-header">
<h2>体检报告</h2>
<div class="actions">
<el-button @click="downloadReport" icon="el-icon-download">
下载报告
</el-button>
<el-button @click="printReport" icon="el-icon-printer">
打印报告
</el-button>
<el-button @click="shareWithFamily" icon="el-icon-share">
分享给家属
</el-button>
</div>
</div>
<!-- PDF预览 -->
<div class="pdf-viewer">
<iframe
v-if="reportUrl"
:src="`/pdfjs/web/viewer.html?file=${encodeURIComponent(reportUrl)}`"
frameborder="0"
width="100%"
height="800px"
/>
<div v-else class="no-report">
<i class="el-icon-document"></i>
<p>报告正在生成中,请稍后查看</p>
</div>
</div>
<!-- 异常报告提醒 -->
<div v-if="report.isAbnormal" class="abnormal-alert">
<el-alert
title="异常报告提醒"
type="warning"
:closable="false"
description="您的体检报告显示有异常指标,请及时就医复查"
show-icon
>
<div slot="title">
<i class="el-icon-warning"></i>
<span style="font-weight: bold; font-size: 18px;">
异常报告提醒
</span>
</div>
</el-alert>
<div class="doctor-suggestion">
<h4>医生建议:</h4>
<p>{{ report.suggestion }}</p>
</div>
</div>
</div>
</div>
</template>
后端文件上传处理:
@RestController
@RequestMapping("/api/report")
@Slf4j
public class HealthReportController {
@Autowired
private HealthReportService reportService;
@PostMapping("/upload")
public Result uploadReport(@RequestParam("file") MultipartFile file,
@RequestParam("appointmentId") Integer appointmentId,
HttpServletRequest request) {
try {
// 1. 文件格式校验
String originalFilename = file.getOriginalFilename();
String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")).toLowerCase();
if (!Arrays.asList(".pdf", ".jpg", ".jpeg", ".png").contains(suffix)) {
return Result.error("只支持PDF、JPG、PNG格式");
}
// 2. 文件大小校验(50MB)
if (file.getSize() > 50 * 1024 * 1024) {
return Result.error("文件大小不能超过50MB");
}
// 3. 生成唯一文件名
String uuid = UUID.randomUUID().toString().replace("-", "");
String newFilename = uuid + suffix;
// 4. 保存到文件服务器(这里用本地目录示例)
String uploadDir = "/data/health_reports/";
File dir = new File(uploadDir);
if (!dir.exists()) {
dir.mkdirs();
}
File destFile = new File(uploadDir + newFilename);
file.transferTo(destFile);
// 5. 生成报告记录(先不保存详细信息)
String reportNo = "RP" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))
+ String.format("%06d", new Random().nextInt(999999));
HealthReport report = new HealthReport();
report.setReportNo(reportNo);
report.setAppointmentId(appointmentId);
report.setFilePath("/reports/" + newFilename); // 访问路径
report.setFileSize(file.getSize());
report.setFileType(suffix.substring(1));
report.setReportDate(LocalDate.now());
// 临时保存,等待医生填写详细信息
reportService.saveTemporaryReport(report);
return Result.success("文件上传成功", reportNo);
} catch (Exception e) {
log.error("体检报告上传失败", e);
return Result.error("文件上传失败:" + e.getMessage());
}
}
@GetMapping("/view/{reportNo}")
@ResponseBody
public Result getReport(@PathVariable String reportNo,
@CurrentUser User user) {
// 权限校验:只有本人、家属、医生可以查看
HealthReport report = reportService.getByReportNo(reportNo);
if (report == null) {
return Result.error("报告不存在");
}
// 检查查看权限
boolean canView = checkViewPermission(user, report);
if (!canView) {
return Result.error("无权查看此报告");
}
// 记录查看日志
reportService.recordView(reportNo, user.getId());
return Result.success(report);
}
private boolean checkViewPermission(User user, HealthReport report) {
// 1. 本人可以查看
if (user.getId().equals(report.getUserId())) {
return true;
}
// 2. 医生可以查看
if ("DOCTOR".equals(user.getRole())) {
return true;
}
// 3. 家属可以查看(如果被授权)
if ("FAMILY".equals(user.getRole())) {
// 检查是否绑定了该老人
OldUser oldUser = oldUserService.getById(report.getUserId());
if (oldUser != null && oldUser.getFamilyMemberIds() != null) {
List<Integer> familyIds = JSON.parseArray(oldUser.getFamilyMemberIds(), Integer.class);
return familyIds.contains(user.getId());
}
}
return false;
}
}
3. 体检提醒系统(定时任务)
关键逻辑:定时扫描、多渠道提醒(短信+微信+APP推送)。
@Component
@Slf4j
public class HealthReminderTask {
@Autowired
private HealthReminderService reminderService;
@Autowired
private SmsService smsService;
@Autowired
private WechatService wechatService;
@Autowired
private AppPushService appPushService;
// 每天8点、20点执行体检提醒
@Scheduled(cron = "0 0 8,20 * * ?")
public void checkReminders() {
log.info("开始执行体检提醒检查");
LocalDateTime now = LocalDateTime.now();
// 1. 查询需要提醒的记录
List<HealthReminder> reminders = reminderService.getPendingReminders(now);
// 2. 分批处理
for (HealthReminder reminder : reminders) {
try {
sendReminder(reminder);
// 3. 更新提醒状态
reminder.setStatus(2); // 已提醒
reminderService.updateById(reminder);
log.info("体检提醒发送成功:{}", reminder.getId());
} catch (Exception e) {
log.error("体检提醒发送失败:{}", reminder.getId(), e);
}
}
}
private void sendReminder(HealthReminder reminder) {
OldUser user = oldUserService.getById(reminder.getUserId());
// 给老人发送(短信)
if (user.getPhone() != null) {
String smsContent = String.format(
"【老年人体检系统】%s,您有%s需要处理:%s",
user.getRealName(),
getReminderTypeText(reminder.getReminderType()),
reminder.getTitle()
);
smsService.sendSms(user.getPhone(), smsContent);
}
// 给家属发送(微信模板消息)
if (user.getFamilyMemberIds() != null) {
List<Integer> familyIds = JSON.parseArray(user.getFamilyMemberIds(), Integer.class);
for (Integer familyId : familyIds) {
FamilyMember family = familyService.getById(familyId);
if (family != null && family.getWechatOpenid() != null) {
wechatService.sendReminderTemplate(
family.getWechatOpenid(),
reminder.getTitle(),
reminder.getContent(),
reminder.getRemindTime()
);
}
}
}
// APP推送
appPushService.sendPush(
user.getId(),
"体检提醒",
reminder.getContent(),
"health_reminder"
);
}
private String getReminderTypeText(Integer type) {
switch (type) {
case 1: return "体检提醒";
case 2: return "用药提醒";
case 3: return "复诊提醒";
default: return "健康提醒";
}
}
}
五、特殊功能:针对老年人的设计
1. 语音播报功能(辅助功能)
// 语音播报工具类
class SpeechHelper {
constructor() {
this.isSupported = 'speechSynthesis' in window;
this.speech = null;
}
// 播报文本
speak(text, options = {}) {
if (!this.isSupported) return false;
// 停止之前的播报
this.stop();
this.speech = new SpeechSynthesisUtterance(text);
// 默认设置(适合老年人)
this.speech.rate = options.rate || 0.8; // 语速慢
this.speech.pitch = options.pitch || 1.0; // 音调
this.speech.volume = options.volume || 1.0; // 音量
this.speech.lang = 'zh-CN'; // 中文
if (options.voice) {
this.speech.voice = options.voice;
}
// 播报开始和结束的回调
this.speech.onstart = () => {
console.log('开始播报');
if (options.onStart) options.onStart();
};
this.speech.onend = () => {
console.log('播报结束');
if (options.onEnd) options.onEnd();
};
window.speechSynthesis.speak(this.speech);
return true;
}
// 停止播报
stop() {
if (this.isSupported) {
window.speechSynthesis.cancel();
}
}
// 获取可用的语音列表
getVoices() {
return new Promise((resolve) => {
if (!this.isSupported) {
resolve([]);
return;
}
let voices = speechSynthesis.getVoices();
if (voices.length) {
resolve(voices);
} else {
speechSynthesis.onvoiceschanged = () => {
voices = speechSynthesis.getVoices();
resolve(voices);
};
}
});
}
// 播报页面主要内容
speakPageContent() {
const mainContent = document.querySelector('main') || document.body;
const text = this.extractText(mainContent);
return this.speak(text);
}
// 提取文本(排除导航、按钮等)
extractText(element) {
// 克隆元素避免修改原DOM
const clone = element.cloneNode(true);
// 移除不需要播报的元素
const excludeSelectors = [
'nav', 'header', 'footer',
'.btn', '.button', 'button',
'.actions', '.action-bar'
];
excludeSelectors.forEach(selector => {
const elements = clone.querySelectorAll(selector);
elements.forEach(el => el.remove());
});
return clone.innerText.replace(/\s+/g, ' ').trim();
}
}
// 在Vue中使用
export default {
mounted() {
this.speechHelper = new SpeechHelper();
// 页面加载后自动播报欢迎语
setTimeout(() => {
this.speechHelper.speak('欢迎使用老年人体检管理系统');
}, 1000);
},
methods: {
// 播报当前页面
speakCurrentPage() {
this.speechHelper.speakPageContent();
},
// 播报重要通知
speakImportantNotice(notice) {
this.speechHelper.speak(`重要通知:${notice}`, {
rate: 0.7, // 更慢的语速
onEnd: () => {
// 播报完后询问是否重复
this.askRepeatNotice(notice);
}
});
},
askRepeatNotice(notice) {
// 弹出确认框(大字体)
this.$confirm({
title: '是否重复播报?',
message: '如果您没有听清楚,可以点击"确定"重复播报',
confirmButtonText: '确定',
cancelButtonText: '取消',
customClass: 'big-dialog' // 自定义大字体对话框
}).then(() => {
this.speechHelper.speak(`重复播报:${notice}`);
});
}
}
}
2. 简化预约流程(三步完成)
<template>
<!-- 三步预约流程 -->
<div class="simple-appointment">
<!-- 步骤指示器 -->
<div class="steps">
<div :class="['step', currentStep === 1 ? 'active' : '']">
<div class="step-number">1</div>
<div class="step-text">选择项目</div>
</div>
<div :class="['step', currentStep === 2 ? 'active' : '']">
<div class="step-number">2</div>
<div class="step-text">选择时间</div>
</div>
<div :class="['step', currentStep === 3 ? 'active' : '']">
<div class="step-number">3</div>
<div class="step-text">确认预约</div>
</div>
</div>
<!-- 步骤1:选择项目(大图标) -->
<div v-show="currentStep === 1" class="step-content">
<h2>请选择体检项目</h2>
<div class="item-options">
<div v-for="item in simpleItems" :key="item.id"
class="item-option" @click="selectItem(item)">
<div class="item-icon">
<i :class="item.icon"></i>
</div>
<div class="item-name">{{ item.name }}</div>
<div class="item-desc">{{ item.desc }}</div>
</div>
</div>
<button class="next-btn" @click="goNext">下一步</button>
</div>
<!-- 步骤2:选择时间(大日历) -->
<div v-show="currentStep === 2" class="step-content">
<h2>请选择体检时间</h2>
<div class="calendar-big">
<div class="calendar-header">
<button @click="prevMonth">上一月</button>
<span>{{ currentMonth }}月</span>
<button @click="nextMonth">下一月</button>
</div>
<div class="calendar-grid">
<div v-for="day in days" :key="day.date"
:class="['day-cell', day.type]"
@click="selectDate(day)">
<div class="day-number">{{ day.day }}</div>
<div v-if="day.available" class="day-status">可约</div>
</div>
</div>
</div>
<div class="time-slots">
<div v-for="slot in timeSlots" :key="slot"
:class="['time-slot', selectedTime === slot ? 'selected' : '']"
@click="selectTime(slot)">
{{ slot }}
</div>
</div>
<div class="step-actions">
<button class="prev-btn" @click="goPrev">上一步</button>
<button class="next-btn" @click="goNext">下一步</button>
</div>
</div>
<!-- 步骤3:确认信息 -->
<div v-show="currentStep === 3" class="step-content">
<h2>请确认预约信息</h2>
<div class="confirm-info">
<div class="info-item">
<label>体检项目:</label>
<span>{{ selectedItem.name }}</span>
</div>
<div class="info-item">
<label>体检时间:</label>
<span>{{ selectedDate }} {{ selectedTime }}</span>
</div>
<div class="info-item">
<label>体检地点:</label>
<span>{{ selectedHospital }}</span>
</div>
</div>
<div class="step-actions">
<button class="prev-btn" @click="goPrev">上一步</button>
<button class="submit-btn" @click="submitAppointment">
确认预约
</button>
</div>
</div>
</div>
</template>
六、系统测试:重点测试老年用户场景
特殊测试用例
| 测试场景 | 测试步骤 | 预期结果 | 重要性 |
|---|---|---|---|
| 字体大小 | 在不同设备上查看页面 | 所有字体不小于16px | 高 |
| 按钮大小 | 点击所有操作按钮 | 按钮足够大,容易点击 | 高 |
| 语音播报 | 点击语音播报按钮 | 清晰播报页面内容 | 中 |
| 简化流程 | 走完体检预约全流程 | 不超过5步完成预约 | 高 |
| 家属协助 | 家属登录查看老人信息 | 只能看到授权信息 | 高 |
| 异常提醒 | 上传异常体检报告 | 老人和家属都收到提醒 | 高 |
兼容性测试重点
- 浏览器:主要测试Chrome、微信内置浏览器(很多老年人用微信访问)。
- 设备:测试手机、平板、电脑,确保响应式布局正常。
- 网络:测试弱网环境下页面加载速度。
七、部署方案:考虑医疗机构实际情况
1. 两种部署方案
方案一:云部署(推荐)
- 服务器:阿里云/腾讯云2核4G(约¥100/月)
- 数据库:云数据库MySQL(自带备份)
- 存储:对象存储OSS(存体检报告)
- 优势:维护简单,扩展方便
方案二:本地部署
- 服务器:医院自有机房
- 数据库:MySQL集群
- 存储:本地NAS存储
- 优势:数据完全可控
2. 一键部署脚本
#!/bin/bash
# deploy-health-system.sh
echo "开始部署老年人体检管理系统..."
# 环境检查
check_environment() {
echo "检查环境..."
if ! command -v java &> /dev/null; then
echo "Java未安装,开始安装JDK 1.8..."
install_java
fi
if ! command -v mysql &> /dev/null; then
echo "MySQL未安装,开始安装MySQL 8.0..."
install_mysql
fi
}
# 数据库初始化
init_database() {
echo "初始化数据库..."
mysql -uroot -p$DB_PASSWORD <<EOF
CREATE DATABASE IF NOT EXISTS old_health CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE old_health;
# 导入SQL文件
source /opt/health-system/sql/init.sql;
EOF
}
# 应用部署
deploy_application() {
echo "部署应用程序..."
# 创建目录
mkdir -p /opt/health-system/{app,logs,reports,backup}
# 备份旧版本
if [ -f "/opt/health-system/app/health-system.jar" ]; then
cp /opt/health-system/app/health-system.jar /opt/health-system/backup/health-system-$(date +%Y%m%d%H%M%S).jar
fi
# 复制新版本
cp target/health-system.jar /opt/health-system/app/
# 复制配置文件
cp application-prod.yml /opt/health-system/app/
# 设置文件权限(体检报告目录)
chmod 777 /opt/health-system/reports
# 启动应用
cd /opt/health-system/app
nohup java -jar health-system.jar --spring.profiles.active=prod > ../logs/app.log 2>&1 &
echo "应用启动PID: $!"
}
# Nginx配置
setup_nginx() {
echo "配置Nginx..."
cat > /etc/nginx/conf.d/health.conf <<EOF
server {
listen 80;
server_name health.your-hospital.com;
# 静态资源
location /reports/ {
alias /opt/health-system/reports/;
expires 30d;
add_header Cache-Control "public";
}
location /static/ {
alias /opt/health-system/app/static/;
expires 30d;
}
# API反向代理
location /api/ {
proxy_pass http://localhost:8080;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
# 前端页面
location / {
root /opt/health-system/app/static;
try_files \$uri \$uri/ /index.html;
}
}
EOF
nginx -s reload
}
main() {
check_environment
init_database
deploy_application
setup_nginx
echo "部署完成!访问地址:http://health.your-hospital.com"
}
main
八、答辩准备:突出适老化设计
-
演示流程要完整: “大家好,我演示老年人体检系统的核心流程。首先,70岁的李大爷登录系统(展示大字体界面),系统根据他的年龄推荐‘老年人全面体检套餐’(展示智能推荐)。李大爷点击‘一键预约’(展示大按钮),选择明天上午(展示大日历),确认信息后提交。然后,医生张医生登录审核(展示医生后台),审核通过后上传体检报告(展示PDF上传和预览)。最后,李大爷和绑定的家属都收到体检报告和健康建议(展示多渠道通知)。”
-
重点讲“适老化设计”:
- “我专门为老年人设计了超大字体(不小于16px)和超大按钮。”
- “增加了语音播报功能,帮助视力不好的老年人使用。”
- “预约流程简化到最多3步,降低操作难度。”
- “家属协助功能,子女可以远程帮助父母操作。”
-
准备好问答:
- Q:老年人不会用智能手机怎么办? A:系统支持家属协助操作,子女可以远程帮忙。也可以考虑与社区合作,由社区工作人员协助操作。
- Q:体检数据安全如何保证? A:敏感数据加密存储,严格权限控制,操作日志完整记录,符合医疗数据安全规范。
- Q:如何保证系统稳定性? A:采用微服务架构,关键服务有熔断降级机制,数据库有主从备份,重要数据定期备份。
最后:一点真心话
老年人体检管理系统不仅要技术过关,更要有人文关怀。在开发过程中,我特意咨询了家里的长辈,了解他们使用手机App的困难和需求。字体大一点、流程简单一点、操作容易一点,这“三点”原则比任何复杂功能都重要。
需要完整源码、适老化UI组件库、部署文档的宝子,可以在评论区留言。
觉得这篇干货有帮助,记得点赞收藏!祝大家毕设顺利,都能做出有温度的毕业设计!👴👵