作为一名长期奋战在医疗信息化一线的开发者,我深知传统医院挂号系统的痛点:患者凌晨排队、号源信息不透明、医院管理效率低下。今天要分享的是基于Spring Boot的全端协同预约挂号系统,这套方案已经在三家二甲医院稳定运行超过两年。
这个系统最核心的价值在于实现了"一个后台服务,多端统一体验"。后端采用Spring Boot构建RESTful API,管理端使用Vue3+Element Plus,患者端同时支持Web门户和微信小程序。特别值得一提的是患者端的双入口设计,既照顾了年轻用户的移动端习惯,又保留了中老年用户习惯的网页操作方式。
选择Spring Boot 2.7.18版本是经过严格考量的结果。这个长期支持版本在稳定性和新特性之间取得了完美平衡。整个后端采用经典的分层架构:
java复制// 典型的Controller示例
@RestController
@RequestMapping("/api/mp/appointment")
public class AppointmentController {
@Autowired
private AppointmentService appointmentService;
@PostMapping
public ResponseResult create(@RequestBody AppointmentCreateDTO dto) {
return ResponseResult.success(appointmentService.create(dto));
}
}
系统采用JWT+Spring Security的无状态认证方案,这是经过多个项目验证的可靠组合。特别设计了双通道认证:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**", "/api/mp/auth/**").permitAll()
.antMatchers("/api/**").hasAnyRole("ADMIN", "DOCTOR")
.antMatchers("/api/mp/**").hasRole("PATIENT")
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
MySQL数据库设计遵循了几个重要原则:
sql复制CREATE TABLE `schedules` (
`id` bigint NOT NULL AUTO_INCREMENT,
`doctor_id` bigint NOT NULL COMMENT '医生ID',
`department_id` bigint NOT NULL COMMENT '科室ID',
`schedule_date` date NOT NULL COMMENT '出诊日期',
`time_slot` varchar(20) NOT NULL COMMENT '时间段',
`total_count` int NOT NULL COMMENT '总号数',
`remain_count` int NOT NULL COMMENT '剩余号数',
`version` int NOT NULL DEFAULT '0' COMMENT '乐观锁版本',
PRIMARY KEY (`id`),
KEY `idx_doctor_date` (`doctor_id`,`schedule_date`),
KEY `idx_dept_date` (`department_id`,`schedule_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这是系统最核心也是最复杂的业务逻辑,我们采用了"先扣减后支付"的模式。关键点在于保证在高并发场景下号源数据的准确性。
java复制@Transactional
public Appointment createAppointment(AppointmentCreateDTO dto) {
// 1. 检查号源可用性
Schedule schedule = scheduleRepository.findById(dto.getScheduleId())
.orElseThrow(() -> new BusinessException("号源不存在"));
// 2. 乐观锁控制
int updated = scheduleRepository.reduceRemainCount(
schedule.getId(), schedule.getVersion());
if(updated == 0) {
throw new BusinessException("号源已被占用");
}
// 3. 创建预约记录
Appointment appointment = new Appointment();
appointment.setAppointmentNo(generateAppointmentNo());
appointment.setStatus(AppointmentStatus.WAIT_PAYMENT);
// 其他字段设置...
return appointmentRepository.save(appointment);
}
重要提示:在实际部署时,我们发现单纯依赖数据库乐观锁在极端高并发下仍可能出现超卖,最终解决方案是结合Redis分布式锁,在扣减前先获取锁。
为了让Portal和小程序能共享同一套API,我们设计了特殊的请求处理机制:
javascript复制// 小程序端的请求封装
const request = (options) => {
return new Promise((resolve, reject) => {
const header = { 'Content-Type': 'application/json' }
if (options.needAuth) {
header['Authorization'] = `Bearer ${getToken()}`
}
wx.request({
url: baseUrl + options.url,
method: options.method || 'GET',
data: options.data,
header,
success: (res) => {
if (res.data.code === 401) {
// 跳转到登录页
navigateToLogin()
return
}
resolve(res.data)
},
fail: (err) => {
showToast('网络错误')
reject(err)
}
})
})
}
医院管理员经常需要批量生成未来一段时间的号源,我们开发了智能生成功能:
java复制public List<Schedule> batchGenerateSchedules(ScheduleBatchDTO dto) {
List<Schedule> result = new ArrayList<>();
LocalDate current = dto.getStartDate();
while (!current.isAfter(dto.getEndDate())) {
// 跳过周末
if (dto.isSkipWeekend() &&
(current.getDayOfWeek() == SATURDAY ||
current.getDayOfWeek() == SUNDAY)) {
current = current.plusDays(1);
continue;
}
for (TimeSlotConfig slot : dto.getTimeSlots()) {
if (!existsSchedule(dto.getDoctorId(), current, slot.getTime())) {
Schedule schedule = createSchedule(dto, current, slot);
result.add(schedule);
}
}
current = current.plusDays(1);
}
return scheduleRepository.saveAll(result);
}
在上线初期,热门专家的号源在放号时经常出现超卖现象。我们通过三级防护解决了这个问题:
当患者在Portal取消预约后,小程序需要实时更新状态。我们采用两种方案:
java复制@GetMapping("/updates")
public SseEmitter subscribeUpdates(@RequestHeader("Authorization") String token) {
String userId = jwtUtil.getUserIdFromToken(token);
SseEmitter emitter = new SseEmitter(30 * 60 * 1000L);
sseEmitters.put(userId, emitter);
emitter.onCompletion(() -> sseEmitters.remove(userId));
emitter.onTimeout(() -> sseEmitters.remove(userId));
return emitter;
}
病历数据有严格的隐私要求,我们实现了:
根据我们的经验,建议如下生产环境配置:
| 组件 | 配置要求 | 说明 |
|---|---|---|
| 应用服务器 | 4核8G内存,SSD磁盘 | 建议至少2台做负载均衡 |
| MySQL数据库 | 8核16G内存,RAID10 SSD阵列 | 主从复制配置 |
| Redis缓存 | 2核4G内存 | 持久化开启 |
在application.yml中这些配置特别重要:
yaml复制server:
tomcat:
max-threads: 200
min-spare-threads: 20
spring:
datasource:
hikari:
maximum-pool-size: 30
connection-timeout: 30000
jpa:
properties:
hibernate:
jdbc:
batch_size: 50
order_inserts: true
order_updates: true
我们使用Prometheus+Grafana搭建监控系统,关键监控指标包括:
在实际运行中,我们总结了几个有价值的扩展方向:
特别值得一提的是微信小程序的消息订阅功能,可以实现:
javascript复制// 小程序订阅消息示例
function subscribe() {
wx.requestSubscribeMessage({
tmplIds: ['模板ID1', '模板ID2'],
success(res) {
console.log('订阅成功', res)
}
})
}
这个项目给我的最大启示是:医疗信息化系统不仅要考虑技术实现,更要理解医疗行业的特殊性和用户的使用习惯。比如我们最初设计的号源选择界面很"技术化",后来根据护士长的建议改成了更符合医护人员思维的方式,使用率立即提升了40%。