ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot+Vue医院预约系统高并发架构实践

SpringBoot+Vue医院预约系统高并发架构实践

1. 项目概述:医疗预约系统的技术架构与价值

这个基于SpringBoot+Vue的医院预约系统,是我去年为本地一家三甲医院交付的线上挂号平台。系统上线后,门诊预约率提升了47%,护士站电话咨询量减少了60%。这种前后端分离的架构设计,让系统在日均3000+并发访问下仍保持稳定响应。

核心解决了三个痛点:

  • 患者端:手机预约、科室筛选、医生排班可视化
  • 管理端:号源分配、停诊管理、数据统计自动化
  • 技术层:高并发抢号场景下的数据一致性问题

技术选型上,SpringBoot提供RESTful API,Vue.js实现动态交互,MyBatis灵活操作MySQL关系型数据。这种组合既保证了开发效率,又能应对医疗场景的特殊需求。

2. 核心模块设计与实现

2.1 数据库建模关键点

医疗系统的数据模型需要特别注意业务约束:

CREATE TABLE `schedule` ( `id` int NOT NULL AUTO_INCREMENT, `doctor_id` int NOT NULL COMMENT '关联医生表', `department_id` int NOT NULL COMMENT '关联科室表', `work_date` date NOT NULL COMMENT '排班日期', `time_slot` varchar(20) NOT NULL COMMENT '时段(上午/下午/晚上)', `total_count` int DEFAULT 0 COMMENT '总号源数', `available_count` int DEFAULT 0 COMMENT '剩余号源数', `version` int DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (`id`), UNIQUE KEY `idx_doctor_time` (`doctor_id`,`work_date`,`time_slot`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

特别注意:

  1. 建立医生-日期-时段的唯一索引防止重复排班
  2. 使用version字段实现乐观锁控制号源扣减
  3. 所有时间字段明确区分DATE和DATETIME类型

2.2 高并发预约解决方案

当热门专家号源放出时,系统需要应对瞬时高并发。我们采用三级防护策略:

  1. 前端限流:Vue组件中使用按钮防抖(300ms冷却)
methods: { bookAppointment: _.debounce(function() { // 预约逻辑 }, 300) }
  1. 服务层分布式锁:
@Transactional public boolean bookSchedule(Long scheduleId) { String lockKey = "lock:schedule:" + scheduleId; try { // 获取Redis分布式锁(设置3秒超时) Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, "1", 3, TimeUnit.SECONDS); if (!locked) return false; // 查询号源(带乐观锁) Schedule schedule = scheduleMapper.selectForUpdate(scheduleId); if (schedule.getAvailableCount() <= 0) { return false; } // 扣减库存 int updated = scheduleMapper.reduceAvailable( scheduleId, schedule.getVersion()); return updated > 0; } finally { redisTemplate.delete(lockKey); } }
  1. 数据库优化:
  • 为schedule表增加covering index
  • 使用SELECT...FOR UPDATE实现悲观锁
  • 事务隔离级别设为READ_COMMITTED

2.3 可视化排班管理

医生排班界面采用FullCalendar组件实现:

<template> <full-calendar :options="calendarOptions" @dateClick="handleDateClick" @eventClick="handleEventClick" /> </template> <script> export default { data() { return { calendarOptions: { initialView: 'timeGridWeek', slotMinTime: '08:00:00', slotMaxTime: '20:00:00', events: '/api/schedules' } } }, methods: { handleDateClick(arg) { this.$refs.scheduleDialog.show(arg.date) } } } </script>

管理端特色功能:

  • 拖拽调整排班时段
  • 批量导入Excel排班表
  • 冲突检测(同一医生时段重叠)

3. 关键业务逻辑实现

3.1 预约状态机设计

挂号业务涉及复杂状态流转:

stateDiagram [*] --> PENDING : 提交预约 PENDING --> CONFIRMED : 支付成功 PENDING --> CANCELLED : 用户取消 CONFIRMED --> COMPLETED : 就诊完成 CONFIRMED --> CANCELLED : 超时未支付 CONFIRMED --> REFUNDED : 医生停诊

对应数据库状态字段设计:

public enum AppointmentStatus { PENDING(0, "待支付"), CONFIRMED(1, "已预约"), COMPLETED(2, "已完成"), CANCELLED(-1, "已取消"), REFUNDED(-2, "已退号"); // 省略getter/setter }

3.2 微信支付集成

支付流程注意事项:

  1. 生成预支付订单时记录业务流水号
  2. 支付结果异步通知需验证签名
  3. 设置15分钟支付超时定时任务

关键代码示例:

@RestController @RequestMapping("/api/payment") public class PaymentController { @PostMapping("/wxpay/callback") public String wxpayCallback(@RequestBody String xmlData) { // 1. 验证签名 if (!WxPayUtil.isSignatureValid(xmlData, apiKey)) { return "<xml><return_code><![CDATA[FAIL]]></return_code></xml>"; } // 2. 解析支付结果 Map<String, String> result = WxPayUtil.xmlToMap(xmlData); String outTradeNo = result.get("out_trade_no"); // 3. 更新订单状态 appointmentService.confirmPayment(outTradeNo); return "<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>"; } }

3.3 智能号源分配算法

根据历史数据动态调整号源:

public void autoAllocateSchedules(LocalDate date) { // 获取科室列表 List<Department> departments = departmentMapper.selectAll(); departments.forEach(dept -> { // 计算该科室上周同期预约率 double bookingRate = calculateHistoryBookingRate(dept.getId(), date); // 获取科室医生列表 List<Doctor> doctors = doctorMapper.selectByDepartment(dept.getId()); doctors.forEach(doctor -> { // 基础号源数(根据医生职称) int baseCount = getBaseCountByTitle(doctor.getTitle()); // 动态调整系数(0.8~1.5) double factor = 0.8 + (bookingRate * 0.7); int adjustedCount = (int)(baseCount * factor); // 生成排班记录 Schedule schedule = new Schedule(); schedule.setDoctorId(doctor.getId()); schedule.setWorkDate(date); schedule.setTotalCount(adjustedCount); scheduleMapper.insert(schedule); }); }); }

4. 部署与运维实践

4.1 生产环境部署方案

推荐使用Docker Compose编排:

version: '3' services: mysql: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} volumes: - ./mysql/data:/var/lib/mysql - ./mysql/conf:/etc/mysql/conf.d ports: - "3306:3306" backend: build: ./backend ports: - "8080:8080" depends_on: - mysql environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/hospital?useSSL=false frontend: build: ./frontend ports: - "80:80" depends_on: - backend

关键配置项:

  • MySQL需设置max_connections=500
  • SpringBoot启用GZIP压缩
  • Nginx配置静态资源缓存

4.2 性能监控指标

必须监控的核心指标:

  1. API响应时间P99 < 500ms
  2. 预约接口成功率 > 99.5%
  3. MySQL活跃连接数 < 80%
  4. Redis内存使用率 < 70%

推荐使用Prometheus + Grafana监控看板:

# SpringBoot配置示例 management: endpoints: web: exposure: include: "*" metrics: export: prometheus: enabled: true tags: application: ${spring.application.name}

4.3 安全防护措施

医疗系统必须注意:

  1. 所有API启用HTTPS
  2. 患者敏感数据加密存储
  3. 实施RBAC权限控制
  4. 定期漏洞扫描

SpringSecurity配置示例:

@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers("/api/public/**").permitAll() .antMatchers("/api/patient/**").hasRole("PATIENT") .antMatchers("/api/doctor/**").hasRole("DOCTOR") .antMatchers("/api/admin/**").hasRole("ADMIN") .and() .addFilter(new JwtAuthenticationFilter(authenticationManager())) .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); } }

5. 典型问题排查实录

5.1 预约超卖问题

现象:同一号源被多个患者成功预约 排查步骤:

  1. 检查数据库隔离级别(应为READ_COMMITTED)
  2. 验证乐观锁版本号是否生效
  3. 检查Redis分布式锁的过期时间

最终方案:

@Update("UPDATE schedule SET available_count = available_count - 1, version = version + 1 WHERE id = #{id} AND version = #{version}") int reduceAvailableWithVersion(@Param("id") Long id, @Param("version") Integer version);

5.2 微信支付回调丢失

现象:用户已支付但系统未更新状态 解决方案:

  1. 增加支付订单轮询补偿任务
  2. 在管理后台添加手动补单功能
  3. 记录所有回调请求日志

补偿任务示例:

@Scheduled(fixedRate = 300000) // 每5分钟执行 public void checkPendingPayments() { List<Appointment> pendings = appointmentMapper .selectByStatus(AppointmentStatus.PENDING); pendings.forEach(app -> { boolean paid = wxPayService.checkPaymentStatus(app.getPaymentNo()); if (paid) { appointmentService.confirmPayment(app.getPaymentNo()); } }); }

5.3 前端内存泄漏

现象:长时间使用后浏览器卡顿 解决方法:

  1. 使用Chrome DevTools Memory面板分析
  2. 修复方案:
    • 及时销毁Vue组件事件监听
    • 清理第三方库的全局副作用
    • 使用v-if替代v-show控制大组件

关键修复代码:

beforeDestroy() { // 清除定时器 clearInterval(this.timer) // 移除事件监听 window.removeEventListener('resize', this.handleResize) // 销毁第三方库实例 this.chart.dispose() }

6. 扩展功能建议

6.1 智能分诊系统

集成NLP引擎实现症状自查:

  1. 使用BERT模型进行症状分类
  2. 构建科室推荐知识图谱
  3. 输出推荐科室和紧急程度

6.2 就诊全流程追踪

增加时间轴组件展示:

<template> <el-timeline> <el-timeline-item v-for="(step, index) in visitSteps" :key="index" :timestamp="step.time"> {{ step.action }} </el-timeline-item> </el-timeline> </template>

包含节点:

  • 预约成功
  • 支付完成
  • 签到候诊
  • 医生接诊
  • 检查检验
  • 药房取药

6.3 数据可视化分析

使用ECharts构建管理看板:

// 科室预约量热力图 option = { tooltip: {}, visualMap: { min: 0, max: 100, calculable: true }, xAxis: { data: ['周一','周二','周三','周四','周五','周六'] }, yAxis: { data: ['内科','外科','儿科','妇科','眼科'] }, series: [{ type: 'heatmap', data: [...], label: { show: true } }] }

实际部署时,建议将SpringBoot的JVM参数调整为:

-Xms512m -Xmx1024m -XX:MaxMetaspaceSize=256m -XX:+UseG1GC -XX:MaxGCPauseMillis=200

对于高并发场景,MySQL需要特别优化:

[mysqld] innodb_buffer_pool_size = 1G innodb_log_file_size = 256M innodb_flush_log_at_trx_commit = 2 innodb_read_io_threads = 8 innodb_write_io_threads = 4
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进