作为一名经历过多次医疗系统开发的Java工程师,我深知传统线下挂号模式的痛点。去年参与某三甲医院预约系统改造时,亲眼目睹了患者凌晨排队、黄牛倒号、医护超负荷工作的乱象。基于SpringBoot的诊疗预约平台正是为解决这些问题而设计的现代化解决方案。
这个平台本质上是一个B/S架构的医疗预约中台,核心目标是实现"患者少跑腿、信息多跑路"。通过角色权限分离,我们为患者提供24小时在线预约服务,为医生智能排班减负,为管理员提供可视化管控工具。相比传统C/S架构系统,采用SpringBoot+Vue的前后端分离方案,在开发效率、可维护性和用户体验上都有显著提升。
选择SpringBoot 2.7.x版本主要基于三个实际考量:
@SpringBootApplication一个注解即可启动服务关键依赖示例:
xml复制<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
采用Vue 3.x + Element Plus的组合出于以下考虑:
典型组件封装逻辑:
javascript复制// 预约日历组件
export default {
props: ['doctorId'],
data() {
return {
availableSlots: []
}
},
mounted() {
this.loadSchedule()
},
methods: {
async loadSchedule() {
const res = await getDoctorSchedule(this.doctorId)
this.availableSlots = res.data
}
}
}
MySQL 8.0相比5.7版本主要利用了这些特性:
核心表关系设计:
sql复制CREATE TABLE `t_user` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`username` VARCHAR(64) UNIQUE NOT NULL COMMENT '登录账号',
`password` VARCHAR(128) NOT NULL COMMENT 'BCrypt加密密码',
`real_name` VARCHAR(32) NOT NULL COMMENT '实名信息',
`role_type` TINYINT NOT NULL COMMENT '1患者 2医生 3管理员'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `t_schedule` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`doctor_id` BIGINT NOT NULL COMMENT '医生ID',
`work_date` DATE NOT NULL COMMENT '排班日期',
`time_slots` JSON NOT NULL COMMENT '时间段配置',
FOREIGN KEY (`doctor_id`) REFERENCES `t_user`(`id`)
);
采用RBAC模型结合Spring Security实现三级权限隔离:
权限拦截器关键代码:
java复制@PreAuthorize("hasRole('DOCTOR')")
@GetMapping("/api/patient-records")
public Result<List<PatientRecord>> getPatientRecords(
@RequestParam Long patientId) {
// 医生只能查看自己关联的患者记录
return Result.success(recordService.getRecords(patientId));
}
踩坑提醒:权限注解要配合方法级参数校验,避免越权访问
采用状态机模式管理预约生命周期:
mermaid复制stateDiagram
[*] --> PENDING
PENDING --> CONFIRMED: 医生确认
PENDING --> CANCELLED: 患者取消
CONFIRMED --> COMPLETED: 就诊完成
CONFIRMED --> CANCELLED: 超时未就诊
对应实体状态字段设计:
java复制public enum AppointmentStatus {
PENDING(0, "待确认"),
CONFIRMED(1, "已预约"),
CANCELLED(2, "已取消"),
COMPLETED(3, "已完成");
// 省略value/code转换方法
}
针对专家号秒杀场景,采用三级防护:
sql复制UPDATE t_appointment
SET version = version + 1
WHERE id = ? AND version = ?
java复制public boolean tryLock(String key, long expireTime) {
return redisTemplate.opsForValue()
.setIfAbsent(key, "1", expireTime, TimeUnit.SECONDS);
}
解决方案:通过数据库约束+业务校验双重保障
sql复制ALTER TABLE t_schedule ADD CONSTRAINT uk_doctor_date
UNIQUE (doctor_id, work_date);
业务层校验逻辑:
java复制public void checkScheduleConflict(Long doctorId, LocalDate date) {
if (scheduleMapper.existsByDoctorAndDate(doctorId, date)) {
throw new BusinessException("该医生当日已有排班");
}
}
使用Spring Scheduled实现定时任务:
java复制@Scheduled(cron = "0 0/30 * * * ?")
public void cancelTimeoutAppointments() {
List<Appointment> timeoutList = appointmentMapper
.selectTimeoutList(LocalDateTime.now().minusMinutes(30));
timeoutList.forEach(app -> {
app.setStatus(AppointmentStatus.CANCELLED);
appointmentMapper.updateById(app);
// 发送通知
notifyService.sendCancelNotice(app);
});
}
通过组合索引+缓存标记实现:
sql复制CREATE INDEX idx_patient_doctor_date ON t_appointment
(patient_id, doctor_id, appointment_date);
缓存校验逻辑:
java复制public boolean checkDuplicateAppointment(Long patientId, Long doctorId, LocalDate date) {
String cacheKey = String.format("appoint:%s:%s:%s",
patientId, doctorId, date);
return redisTemplate.hasKey(cacheKey);
}
application-prod.yml关键配置:
yaml复制server:
tomcat:
max-threads: 200
min-spare-threads: 20
spring:
datasource:
hikari:
maximum-pool-size: 30
connection-timeout: 30000
集成Prometheus + Grafana监控体系:
java复制@Bean
public MeterRegistryCustomizer<PrometheusMeterRegistry> metrics() {
return registry -> registry.config().commonTags("application", "booking-system");
}
采用ELK栈实现日志集中管理:
xml复制<!-- logstash-logback-encoder -->
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.2</version>
</dependency>
logback-spring.xml配置示例:
xml复制<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>logstash:5044</destination>
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>
基于历史数据实现:
python复制# 伪代码示例
def recommend_doctors(patient_id):
history = get_history_appointments(patient_id)
similar_patients = find_similar_patients(history)
return aggregate_recommendations(similar_patients)
改造方案要点:
java复制@PostMapping("/api/wx-login")
public Result<String> wechatLogin(@RequestBody WxLoginDTO dto) {
String openid = wxService.getOpenId(dto.getCode());
User user = userService.getByWxOpenId(openid);
return Result.success(jwtUtil.generateToken(user));
}
潜在拆分方向:
Spring Cloud Alibaba技术选型:
xml复制<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
这个项目从零到上线的过程中,最深刻的体会是:医疗系统对数据一致性和可靠性的要求远超普通电商系统。比如在预约状态变更时,我们最终采用了本地事务表+定时任务补偿的方案,确保即使系统崩溃也不会出现状态不一致。建议后续开发类似系统的同行,在初期就要建立完善的数据审计机制和异常处理预案。