毕业设计实战:SpringBoot老年人体检管理系统,从需求到部署完整指南

53 阅读16分钟

毕业设计实战: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.0JSON字段支持,存体检项目配置方便。一定用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='体检提醒表';

设计亮点

  1. 虚拟列计算年龄:MySQL 5.7+支持生成列,自动计算年龄,不用程序算。
  2. JSON字段存储家属family_member_ids用JSON存储绑定的家属ID,查询方便。
  3. 报告文件分离存储:文件存服务器,数据库只存路径,查询快。
  4. 多重索引优化:常用查询字段都加了索引。

四、功能实现:抓住老年人体检的特殊需求

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

八、答辩准备:突出适老化设计

  1. 演示流程要完整: “大家好,我演示老年人体检系统的核心流程。首先,70岁的李大爷登录系统(展示大字体界面),系统根据他的年龄推荐‘老年人全面体检套餐’(展示智能推荐)。李大爷点击‘一键预约’(展示大按钮),选择明天上午(展示大日历),确认信息后提交。然后,医生张医生登录审核(展示医生后台),审核通过后上传体检报告(展示PDF上传和预览)。最后,李大爷和绑定的家属都收到体检报告和健康建议(展示多渠道通知)。”

  2. 重点讲“适老化设计”

    • “我专门为老年人设计了超大字体(不小于16px)和超大按钮。”
    • “增加了语音播报功能,帮助视力不好的老年人使用。”
    • “预约流程简化到最多3步,降低操作难度。”
    • “家属协助功能,子女可以远程帮助父母操作。”
  3. 准备好问答

    • Q:老年人不会用智能手机怎么办? A:系统支持家属协助操作,子女可以远程帮忙。也可以考虑与社区合作,由社区工作人员协助操作。
    • Q:体检数据安全如何保证? A:敏感数据加密存储,严格权限控制,操作日志完整记录,符合医疗数据安全规范。
    • Q:如何保证系统稳定性? A:采用微服务架构,关键服务有熔断降级机制,数据库有主从备份,重要数据定期备份。

最后:一点真心话

老年人体检管理系统不仅要技术过关,更要有人文关怀。在开发过程中,我特意咨询了家里的长辈,了解他们使用手机App的困难和需求。字体大一点、流程简单一点、操作容易一点,这“三点”原则比任何复杂功能都重要。

需要完整源码适老化UI组件库部署文档的宝子,可以在评论区留言。

觉得这篇干货有帮助,记得点赞收藏!祝大家毕设顺利,都能做出有温度的毕业设计!👴👵