1. 项目概述与背景
医院管理系统是现代医疗机构数字化转型的核心基础设施。作为一名长期从事医疗信息化系统开发的工程师,我最近完成了一个基于Vue3+SpringBoot的医院管理系统项目。这个系统采用了前后端分离架构,实现了患者、医生和管理员三大角色的全业务流程管理。
在医疗行业信息化建设过程中,传统单体架构的系统已经难以满足现代医院的高并发、高可用需求。我们团队经过多次技术选型讨论,最终确定了Vue3+SpringBoot的技术组合。Vue3的响应式特性和Composition API能够很好地支撑复杂的前端交互,而SpringBoot的快速开发特性和丰富的生态则能保证后端服务的稳定性。
这个系统最核心的价值在于:
- 实现了患者从预约挂号到就诊的全流程线上化
- 为医生提供了便捷的病患管理和诊断工具
- 帮助管理员高效管理医院各类资源
- 采用分布式架构设计,支持未来业务扩展
2. 系统架构设计
2.1 技术栈选型
在项目启动阶段,我们对比了多种技术方案,最终确定的技术栈如下:
前端技术栈:
- Vue 3.2+:采用Composition API编写组件,代码更清晰
- Element Plus:UI组件库,提供丰富的医疗行业适用组件
- Axios:处理HTTP请求,封装了统一的拦截器
- ECharts:用于数据可视化展示
- Vue Router:实现前端路由管理
- Pinia:状态管理库,替代Vuex
后端技术栈:
- Spring Boot 2.7:快速构建RESTful API
- MyBatis-Plus 3.5:简化数据库操作
- MySQL 8.0:关系型数据库
- Redis 5.0:缓存高频访问数据
- JWT:实现无状态认证
- Swagger:API文档生成
开发环境:
- JDK 1.8
- Node.js 14+
- Maven 3.6.1
- IDEA 2022+ / VSCode
2.2 系统架构图
系统采用经典的三层架构设计:
code复制┌───────────────────────────────────────────────────┐
│ Presentation Layer │
│ (Vue3 + Element Plus + Axios + ECharts) │
└───────────────────────────────────────────────────┘
▲
│ HTTP/HTTPS
▼
┌───────────────────────────────────────────────────┐
│ Business Layer │
│ (Spring Boot + MyBatis-Plus + Redis + JWT) │
└───────────────────────────────────────────────────┘
▲
│ JDBC
▼
┌───────────────────────────────────────────────────┐
│ Data Layer │
│ (MySQL + Redis) │
└───────────────────────────────────────────────────┘
这种分层架构的优势在于:
- 前后端完全解耦,可以独立开发和部署
- 业务逻辑集中处理,便于维护和扩展
- 数据访问层抽象,支持多种数据源
- 清晰的职责划分,提高团队协作效率
3. 数据库设计与实现
3.1 核心表结构设计
数据库设计是系统稳定性的基础。我们采用了MySQL 8.0作为主数据库,设计了以下核心表:
用户表(sys_user):
sql复制CREATE TABLE sys_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
role INT DEFAULT 2 COMMENT '0-管理员,1-医生,2-患者',
real_name VARCHAR(50),
phone VARCHAR(20),
email VARCHAR(50),
status INT DEFAULT 1 COMMENT '1正常 0禁用',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
医生信息表(doctor_info):
sql复制CREATE TABLE doctor_info (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT,
title VARCHAR(50) COMMENT '职称',
department VARCHAR(50) COMMENT '科室',
specialty TEXT COMMENT '专长描述',
price DECIMAL(10,2) COMMENT '挂号费',
schedule_json TEXT COMMENT '排班信息JSON',
avatar VARCHAR(255) COMMENT '头像URL',
FOREIGN KEY (user_id) REFERENCES sys_user(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
挂号记录表(registration):
sql复制CREATE TABLE registration (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
patient_id BIGINT,
doctor_id BIGINT,
reg_date DATE COMMENT '挂号日期',
reg_time DATETIME COMMENT '挂号时间',
status INT DEFAULT 0 COMMENT '0待就诊 1已完成 2已取消',
diagnosis_result TEXT COMMENT '诊断结果',
prescription TEXT COMMENT '处方药品JSON',
check_items TEXT COMMENT '检查项目JSON',
total_fee DECIMAL(10,2) COMMENT '总费用',
pay_status INT DEFAULT 0 COMMENT '0未支付 1已支付',
FOREIGN KEY (patient_id) REFERENCES sys_user(id),
FOREIGN KEY (doctor_id) REFERENCES sys_user(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.2 数据库优化实践
在实际开发中,我们针对医疗系统的特点做了以下数据库优化:
-
索引优化:
- 为所有外键字段添加了索引
- 为高频查询条件(如reg_date, doctor_id)添加联合索引
- 使用EXPLAIN分析慢查询,针对性优化
-
分表策略:
- 挂号记录按月分表(registration_202301, registration_202302)
- 使用MyBatis-Plus的动态表名插件实现透明访问
-
缓存策略:
- 医生排班信息缓存到Redis,设置1小时过期
- 药品目录缓存到Redis,变更时主动更新
-
事务控制:
- 挂号支付采用分布式事务
- 药品库存更新使用乐观锁
4. 后端核心实现
4.1 SpringBoot项目结构
后端项目采用标准的Maven多模块结构:
code复制hospital-backend
├── hospital-common // 公共模块
├── hospital-system // 系统模块
├── hospital-admin // 管理员模块
├── hospital-doctor // 医生模块
├── hospital-patient // 患者模块
└── hospital-gateway // 网关模块
4.2 关键代码实现
统一响应封装(Result.java):
java复制@Data
public class Result<T> implements Serializable {
private Integer code;
private String msg;
private T data;
private Long timestamp = System.currentTimeMillis();
public static <T> Result<T> success() {
return success(null);
}
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMsg("success");
result.setData(data);
return result;
}
public static <T> Result<T> error(Integer code, String msg) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMsg(msg);
return result;
}
}
JWT认证过滤器(JwtAuthenticationFilter.java):
java复制public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = jwtTokenProvider.resolveToken(request);
try {
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication auth = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
} catch (JwtException e) {
SecurityContextHolder.clearContext();
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
return;
}
filterChain.doFilter(request, response);
}
}
挂号服务实现(RegistrationServiceImpl.java):
java复制@Service
@Transactional
public class RegistrationServiceImpl implements RegistrationService {
@Autowired
private RegistrationMapper registrationMapper;
@Autowired
private DoctorInfoMapper doctorInfoMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public Result<?> createRegistration(RegistrationDTO dto) {
// 校验医生排班
String scheduleKey = "doctor:schedule:" + dto.getDoctorId();
String scheduleJson = (String) redisTemplate.opsForValue().get(scheduleKey);
if (scheduleJson == null) {
DoctorInfo doctor = doctorInfoMapper.selectById(dto.getDoctorId());
scheduleJson = doctor.getScheduleJson();
redisTemplate.opsForValue().set(scheduleKey, scheduleJson, 1, TimeUnit.HOURS);
}
// 解析排班信息
List<ScheduleVO> schedules = JSON.parseArray(scheduleJson, ScheduleVO.class);
boolean available = schedules.stream()
.anyMatch(s -> s.getDayOfWeek() == dto.getRegDate().getDayOfWeek()
&& s.getTimeSlot().equals(dto.getTimeSlot()));
if (!available) {
return Result.error(400, "医生该时段不可预约");
}
// 创建挂号记录
Registration registration = new Registration();
BeanUtils.copyProperties(dto, registration);
registration.setRegTime(LocalDateTime.now());
registration.setStatus(0);
registrationMapper.insert(registration);
return Result.success("挂号成功");
}
}
5. 前端核心实现
5.1 Vue3项目结构
前端项目采用Vue3官方推荐结构:
code复制src
├── api // API请求封装
├── assets // 静态资源
├── components // 公共组件
├── composables // 组合式函数
├── router // 路由配置
├── stores // Pinia状态管理
├── styles // 全局样式
├── utils // 工具函数
└── views // 页面组件
5.2 关键组件实现
患者挂号组件(RegistrationForm.vue):
vue复制<template>
<el-card class="registration-form">
<template #header>
<div class="card-header">
<span>预约挂号</span>
</div>
</template>
<el-form :model="form" :rules="rules" ref="formRef">
<el-form-item label="选择科室" prop="department">
<el-select v-model="form.department" placeholder="请选择科室" @change="loadDoctors">
<el-option
v-for="dept in departments"
:key="dept.value"
:label="dept.label"
:value="dept.value"
/>
</el-select>
</el-form-item>
<el-form-item label="选择医生" prop="doctorId">
<el-select v-model="form.doctorId" placeholder="请选择医生">
<el-option
v-for="doctor in doctors"
:key="doctor.id"
:label="`${doctor.realName} (${doctor.title})`"
:value="doctor.id"
>
<div class="doctor-option">
<el-avatar :src="doctor.avatar" size="small" />
<span style="margin-left: 10px;">
{{ doctor.realName }} - {{ doctor.title }}
<el-tag size="small" type="info">{{ doctor.department }}</el-tag>
</span>
</div>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="预约日期" prop="regDate">
<el-date-picker
v-model="form.regDate"
type="date"
placeholder="选择日期"
:disabled-date="disabledDate"
@change="loadTimeSlots"
/>
</el-form-item>
<el-form-item label="预约时段" prop="timeSlot">
<el-radio-group v-model="form.timeSlot">
<el-radio-button
v-for="slot in timeSlots"
:key="slot.value"
:label="slot.value"
:disabled="!slot.available"
>
{{ slot.label }}
</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm">提交预约</el-button>
</el-form-item>
</el-form>
</el-card>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import { bookRegistration } from '@/api/registration'
const formRef = ref()
const doctors = ref([])
const timeSlots = ref([])
const form = reactive({
department: '',
doctorId: '',
regDate: '',
timeSlot: ''
})
const departments = [
{ value: 'internal', label: '内科' },
{ value: 'surgery', label: '外科' },
// 其他科室...
]
const rules = {
department: [{ required: true, message: '请选择科室', trigger: 'change' }],
doctorId: [{ required: true, message: '请选择医生', trigger: 'change' }],
regDate: [{ required: true, message: '请选择日期', trigger: 'change' }],
timeSlot: [{ required: true, message: '请选择时段', trigger: 'change' }]
}
const disabledDate = (time) => {
return time.getTime() < Date.now() - 8.64e7
}
const loadDoctors = async () => {
if (!form.department) return
const res = await getDoctorsByDepartment(form.department)
doctors.value = res.data
}
const loadTimeSlots = async () => {
if (!form.doctorId || !form.regDate) return
const res = await getDoctorSchedule(form.doctorId, form.regDate)
timeSlots.value = res.data
}
const submitForm = async () => {
try {
await formRef.value.validate()
const res = await bookRegistration(form)
if (res.code === 200) {
ElMessage.success('预约成功')
// 跳转到我的挂号页面
}
} catch (error) {
console.error(error)
}
}
</script>
<style scoped>
.doctor-option {
display: flex;
align-items: center;
}
</style>
医生工作台组件(DoctorDashboard.vue):
vue复制<template>
<div class="dashboard-container">
<el-row :gutter="20">
<el-col :span="8">
<el-card class="today-patients">
<template #header>
<div class="card-header">
<span>今日就诊患者</span>
<el-tag type="danger">{{ todayCount }}人</el-tag>
</div>
</template>
<el-table :data="todayPatients" height="300" border>
<el-table-column prop="patientName" label="姓名" width="80" />
<el-table-column prop="regTime" label="时间" width="120" />
<el-table-column prop="symptom" label="主诉" show-overflow-tooltip />
<el-table-column label="操作" width="80">
<template #default="scope">
<el-button
size="small"
type="primary"
@click="startDiagnosis(scope.row)"
>接诊</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
<el-col :span="16">
<el-card class="diagnosis-area">
<template #header>
<div class="card-header">
<span>诊断工作区</span>
</div>
</template>
<div v-if="currentPatient">
<patient-info :patient="currentPatient" />
<diagnosis-form
:patient-id="currentPatient.id"
@submit="handleDiagnosisSubmit"
/>
</div>
<div v-else class="empty-tip">
<el-empty description="请从左侧选择患者开始诊断" />
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getTodayPatients, submitDiagnosis } from '@/api/doctor'
import PatientInfo from './components/PatientInfo.vue'
import DiagnosisForm from './components/DiagnosisForm.vue'
const todayPatients = ref([])
const todayCount = ref(0)
const currentPatient = ref(null)
const loadTodayPatients = async () => {
const res = await getTodayPatients()
todayPatients.value = res.data.list
todayCount.value = res.data.total
}
const startDiagnosis = (patient) => {
currentPatient.value = patient
}
const handleDiagnosisSubmit = async (form) => {
try {
const res = await submitDiagnosis({
regId: currentPatient.value.id,
...form
})
if (res.code === 200) {
ElMessage.success('诊断提交成功')
loadTodayPatients()
currentPatient.value = null
}
} catch (error) {
console.error(error)
}
}
onMounted(() => {
loadTodayPatients()
})
</script>
<style scoped>
.dashboard-container {
padding: 20px;
}
.today-patients {
height: 100%;
}
.empty-tip {
height: 400px;
display: flex;
align-items: center;
justify-content: center;
}
</style>
6. 系统部署与运维
6.1 生产环境部署方案
我们推荐以下部署架构:
code复制用户请求 → Nginx(负载均衡) → 前端静态资源
↓
SpringBoot应用集群(2-4节点)
↓
MySQL主从集群(1主2从)
↓
Redis哨兵集群(3节点)
Nginx配置示例:
nginx复制upstream backend {
server 192.168.1.101:8080;
server 192.168.1.102:8080;
keepalive 32;
}
server {
listen 80;
server_name hospital.example.com;
location / {
root /opt/hospital-frontend/dist;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
6.2 性能优化建议
-
前端优化:
- 使用路由懒加载
- 组件按需引入
- 开启Gzip压缩
- 配置合理的缓存策略
-
后端优化:
- JVM参数调优(-Xms, -Xmx)
- 数据库连接池配置
- 接口响应缓存
- 异步处理耗时操作
-
数据库优化:
- 合理的分库分表策略
- 读写分离
- 定期维护(OPTIMIZE TABLE)
6.3 监控与告警
建议部署以下监控系统:
- Prometheus + Grafana:监控服务器和应用指标
- ELK:日志收集与分析
- SkyWalking:分布式链路追踪
- 自定义健康检查接口:/actuator/health
关键监控指标:
- 接口响应时间(P99 < 500ms)
- 错误率(< 0.1%)
- 系统负载(CPU < 70%)
- 数据库连接池使用率(< 80%)
7. 项目总结与经验分享
在开发这个医院管理系统的过程中,我们积累了一些宝贵的经验:
-
医疗业务特殊性:
- 挂号业务需要考虑并发控制,我们最终采用了Redis分布式锁+乐观锁的方案
- 处方药品需要严格的库存检查,我们实现了预扣减库存机制
- 医生排班变更需要及时通知已挂号患者,我们集成了短信通知功能
-
性能优化经验:
- 挂号高峰期页面加载慢:通过分析发现是医生列表查询未分页,优化后响应时间从2s降到200ms
- 药品目录加载慢:实现二级缓存(Redis+本地缓存),QPS从100提升到3000+
- 报告单生成耗时:改用异步生成+WebSocket通知的方案
-
安全防护措施:
- 敏感数据(如患者病历)加密存储
- 操作日志全记录,满足医疗合规要求
- 定期进行安全扫描和渗透测试
-
扩展性设计:
- 采用微服务架构,核心功能模块化
- 预留了HIS系统对接接口
- 支持多院区部署方案
这个项目从技术选型到最终上线历时6个月,期间遇到了许多挑战,但也收获了很多宝贵的经验。医疗信息化系统不同于一般的业务系统,它对稳定性、安全性和合规性有着极高的要求。通过这个项目,我们团队在医疗行业系统开发方面积累了丰富的经验,为后续类似项目打下了坚实的基础。