1. 项目概述与背景
作为一名经历过多次医疗系统开发的老码农,我深知传统医院挂号系统的痛点。记得去年陪家人去三甲医院就诊,早上6点排队取号,等到9点才挂上下午的号,这种体验促使我着手开发这套在线挂号系统。系统采用SpringBoot+Vue3+MyBatis技术栈,实现了患者足不出户完成预约、医生动态排班管理、科室资源智能分配等核心功能。
这套系统最显著的价值在于:
- 患者端:预约耗时从平均47分钟缩短至2分钟(实测数据)
- 医院端:挂号窗口人力成本降低60%,号源利用率提升35%
- 管理端:实时监控各科室就诊压力,动态调整资源分配
技术选型上,我们放弃了传统的SSM组合,选择SpringBoot 2.7.x+Vue3的组合,主要考虑到:
- SpringBoot的自动配置特性大幅减少XML配置
- Vue3的Composition API更适合复杂前端状态管理
- MyBatis-Plus的ActiveRecord模式简化数据操作
- MySQL 8.0的窗口函数便于生成预约排队序号
2. 系统架构设计
2.1 整体技术架构
系统采用前后端分离架构,这是我经历过的第三代医疗系统设计方案。相比早期JSP方案,这种架构具有明显的性能优势:
前端层:Vue3 + Pinia + Element Plus ↑ HTTP/HTTPS API网关:Spring Cloud Gateway ↑ 内部RPC 业务层:SpringBoot + Spring Security ↑ JDBC 数据层:MyBatis-Plus + MySQL 8.0关键设计决策:
- 使用JWT替代Session,解决分布式会话问题
- 采用RBAC三级权限模型(患者/医生/管理员)
- 预约业务使用乐观锁解决并发冲突
- 诊疗记录采用AES-256加密存储
2.2 数据库设计精要
2.2.1 核心表关系设计
数据库设计经历了三次迭代,最终形成的ER图核心部分如下:
患者表(patient) ←─ 预约表(registration) ─→ 医生表(doctor) ↑ ↑ └───────────────┘ │ 科室表(department)几个关键设计点:
- 患者与医生通过预约表建立多对多关系
- 科室表作为医生表的父表,形成层级结构
- 所有时间字段统一采用UTC+8时区存储
- 状态字段使用TINYINT而非VARCHAR节省空间
2.2.2 表结构优化细节
以医生表为例,我们做了这些特殊处理:
CREATE TABLE `doctor` ( `doctor_id` BIGINT UNSIGNED NOT NULL COMMENT '雪花算法ID', `doctor_name` VARCHAR(50) COLLATE utf8mb4_bin NOT NULL COMMENT '姓名带校验规则', `title_code` SMALLINT NOT NULL COMMENT '职称编码引用字典表', `department_id` BIGINT NOT NULL COMMENT '科室ID带索引', `schedule_json` JSON DEFAULT NULL COMMENT '排班信息', `version` INT DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (`doctor_id`), UNIQUE KEY `idx_phone` (`phone`), KEY `idx_department` (`department_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;特别说明:
- 使用Snowflake算法生成分布式ID
- utf8mb4_bin校对规则确保姓名精确匹配
- JSON类型存储动态排班信息
- 版本号字段实现乐观锁
3. 核心功能实现
3.1 预约挂号业务流程
挂号流程看似简单,实则暗藏多个技术难点。我们的实现方案:
sequenceDiagram participant 患者 participant 前端 participant 网关 participant 业务服务 participant 数据库 患者->>前端: 选择科室/医生 前端->>网关: GET /api/doctors?dept=1 网关->>业务服务: 负载均衡 业务服务->>数据库: 查询可预约医生 业务服务-->>前端: 返回医生列表 患者->>前端: 提交预约信息 前端->>网关: POST /api/registrations 业务服务->>数据库: 开启事务 业务服务->>数据库: 检查号源(乐观锁) 业务服务->>数据库: 创建预约记录 业务服务-->>前端: 返回预约成功关键代码片段(Java部分):
@Transactional public RegistrationResult createRegistration(RegistrationDTO dto) { // 校验预约时间有效性 if (!timeSlotValidator.isValid(dto.getTimeSlot())) { throw new BusinessException("无效的时间段"); } // 使用乐观锁检查号源 Doctor doctor = doctorMapper.selectWithLock(dto.getDoctorId()); if (!doctor.isAvailable(dto.getRegDate())) { throw new BusinessException("号源已满"); } // 生成预约号(科室前缀+日期+序号) String regNumber = numberGenerator.generate( doctor.getDepartment().getCode(), dto.getRegDate() ); // 保存记录 Registration reg = new Registration(); BeanUtils.copyProperties(dto, reg); reg.setRegNumber(regNumber); registrationMapper.insert(reg); // 更新医生可预约数 doctorMapper.updateAvailability( doctor.getDoctorId(), doctor.getVersion() ); return new RegistrationResult(regNumber); }3.2 动态排班管理
医生的排班管理采用了策略模式:
public interface ScheduleStrategy { List<TimeSlot> generateSlots(LocalDate date, Doctor doctor); } @Component @Slf4j public class DefaultScheduleStrategy implements ScheduleStrategy { // 实现默认排班逻辑 } @Component @Slf4j public class EmergencyScheduleStrategy implements ScheduleStrategy { // 实现急诊特殊排班 }在Controller层通过@Qualifier注入不同策略:
@RestController @RequestMapping("/api/schedules") public class ScheduleController { @Autowired @Qualifier("defaultScheduleStrategy") private ScheduleStrategy defaultStrategy; @Autowired @Qualifier("emergencyScheduleStrategy") private ScheduleStrategy emergencyStrategy; @GetMapping("/generate") public List<TimeSlot> generateSlots( @RequestParam String type, @RequestParam Long doctorId, @RequestParam String date) { Doctor doctor = doctorService.getById(doctorId); LocalDate localDate = LocalDate.parse(date); return "emergency".equals(type) ? emergencyStrategy.generateSlots(localDate, doctor) : defaultStrategy.generateSlots(localDate, doctor); } }4. 关键技术实现细节
4.1 高并发号源处理
挂号系统最怕的就是"秒杀"场景。我们采用三级防护:
- 前端防抖:按钮300ms冷却时间
- 令牌桶限流:网关层限制1000次/分钟
- 数据库优化:
- 使用SELECT...FOR UPDATE锁定医生记录
- 号源缓存到Redis,先减库存再落库
- 采用消息队列异步处理支付结果
Redis库存操作示例:
public boolean reduceInventory(Long doctorId, LocalDate date) { String key = "reg:" + doctorId + ":" + date; long remain = redisTemplate.opsForValue().decrement(key); if (remain < 0) { // 库存不足回滚 redisTemplate.opsForValue().increment(key); return false; } return true; }4.2 安全防护措施
医疗系统对安全性要求极高,我们实施了这些措施:
数据传输:
- 全站HTTPS
- 敏感字段二次加密(如身份证号)
数据存储:
@Column(columnDefinition = "VARBINARY(255)") @Convert(converter = AesEncryptConverter.class) private String idCardNumber;权限控制:
- 方法级注解:@PreAuthorize("hasRole('DOCTOR')")
- 数据权限:@DataScope(deptAlias = "d")
审计日志:
@Aspect @Component public class AuditLogAspect { @AfterReturning("execution(* com..service.*.*(..))") public void log(JoinPoint jp) { // 记录操作日志 } }
5. 部署与运维方案
5.1 容器化部署
我们采用Docker Compose编排方案:
version: '3.8' services: app: image: reg-server:${TAG} deploy: resources: limits: cpus: '2' memory: 2G healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"] interval: 30s timeout: 5s retries: 3 mysql: image: mysql:8.0 command: --default-authentication-plugin=mysql_native_password volumes: - mysql_data:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: ${DB_ROOT_PWD} MYSQL_DATABASE: hospital volumes: mysql_data:5.2 监控方案
监控体系搭建经验:
Prometheus配置要点:
scrape_configs: - job_name: 'spring' metrics_path: '/actuator/prometheus' static_configs: - targets: ['app:8080']Grafana看板需要监控的关键指标:
- 预约成功率
- 平均响应时间
- JVM内存使用
- MySQL连接数
日志收集建议:
filebeat.inputs: - type: log paths: - /var/log/app/*.log json.keys_under_root: true
6. 踩坑经验分享
6.1 时区问题
曾因时区问题导致预约日期错误,解决方案:
- 统一使用UTC+8时区
- MySQL配置:
SET GLOBAL time_zone = '+8:00'; - SpringBoot配置:
spring.jpa.properties.hibernate.jdbc.time_zone=Asia/Shanghai
6.2 微信支付回调
微信支付回调的坑:
- 必须处理重复通知
- 需要在5秒内返回成功
- 示例代码:
@PostMapping("/wxpay/callback") public String callback(@RequestBody String xmlData) { // 1. 验签 // 2. 检查订单是否存在 // 3. 处理业务 // 4. 记录已处理 return "<xml><return_code>SUCCESS</return_code></xml>"; }
6.3 性能优化经验
经过压测发现的性能瓶颈及解决方案:
问题:科室列表查询慢(800ms+) 优化:添加复合索引
ALTER TABLE department ADD INDEX idx_parent_status (parent_id, status);问题:预约列表分页慢 优化:使用游标分页
SELECT * FROM registration WHERE id < ? AND patient_id = ? ORDER BY id DESC LIMIT 10问题:医生详情查询N+1 优化:MyBatis关联查询
<resultMap id="doctorDetail" type="DoctorVO"> <association property="department" select="selectDept" column="department_id"/> </resultMap>
这套系统目前已在三家民营医院落地,平均节省患者等待时间40分钟以上。最大的收获是:医疗系统的核心不是技术有多先进,而是要对业务流程有深刻理解。比如我们最初设计的"智能推荐科室"功能,在实际使用中发现老年患者更依赖人工导诊,后来调整为"智能推荐+人工辅助"的双模式才真正提升了用户体验。