作为一名从事Java开发十余年的技术老兵,今天想和大家分享一个基于SpringBoot的社区诊所挂号管理系统开发经验。这个系统是我近期为某三甲医院开发的实际项目,经过半年多的实际运行验证,目前日均处理挂号量超过2000人次,系统稳定性得到了充分验证。
社区诊所挂号管理系统主要解决传统医疗挂号中的三大痛点:
系统采用前后端分离架构,后端基于SpringBoot 2.7.3 + MyBatisPlus 3.5.1,前端使用Vue 3 + Element Plus,数据库选用MySQL 8.0。特别值得一提的是,我们在开发过程中针对医疗行业特点做了多项优化:
系统采用标准的B/S架构,整体分为五层:
这种分层架构的优势在于:
选择SpringBoot主要基于以下考虑:
我们在项目中特别使用了:
java复制@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class
})
这种配置方式让我们可以灵活控制自动配置的加载。
相比原生MyBatis,MyBatisPlus提供了诸多便利:
典型的使用示例:
java复制// 分页查询
Page<Patient> page = new Page<>(1, 10);
LambdaQueryWrapper<Patient> wrapper = Wrappers.lambdaQuery();
wrapper.eq(Patient::getStatus, 1);
patientMapper.selectPage(page, wrapper);
前端采用组合式API写法,主要特点:
典型组件示例:
javascript复制<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
挂号是系统的核心功能,我们设计了状态机来管理挂号生命周期:
code复制[未支付] -> [已支付] -> [待就诊] -> [就诊中] -> [已完成]
|-> [已取消]
关键实现代码:
java复制public class RegistrationService {
@Transactional
public void changeStatus(Long id, RegistrationStatus newStatus) {
Registration registration = getById(id);
if (!registration.getStatus().canTransferTo(newStatus)) {
throw new BusinessException("状态转换不合法");
}
registration.setStatus(newStatus);
updateById(registration);
// 发布领域事件
eventPublisher.publishEvent(new RegistrationStatusChangedEvent(this, id, newStatus));
}
}
号源管理采用分时段的动态分配策略:
数据库设计:
sql复制CREATE TABLE `registration_source` (
`id` bigint NOT NULL AUTO_INCREMENT,
`department_id` bigint NOT NULL COMMENT '科室ID',
`doctor_id` bigint DEFAULT NULL COMMENT '医生ID',
`time_slot` datetime NOT NULL COMMENT '时间段',
`total` int NOT NULL COMMENT '总号源',
`remaining` int NOT NULL COMMENT '剩余号源',
`version` int NOT NULL DEFAULT '0' COMMENT '乐观锁版本号',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_dept_time` (`department_id`,`time_slot`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
支付流程关键点:
支付状态机设计:
java复制public enum PaymentStatus {
UNPAID("未支付"),
PAYING("支付中"),
PAID("支付成功"),
FAILED("支付失败"),
REFUNDING("退款中"),
REFUNDED("已退款");
private final String desc;
// ...
}
缓存策略:
缓存穿透解决方案:
java复制public RegistrationSource getSourceWithCache(Long id) {
String key = "reg:source:" + id;
// 1. 先查缓存
RegistrationSource source = redisTemplate.opsForValue().get(key);
if (source != null) {
return source;
}
// 2. 查数据库
source = registrationSourceMapper.selectById(id);
if (source == null) {
// 缓存空值防止穿透
redisTemplate.opsForValue().set(key, null, 5, TimeUnit.MINUTES);
return null;
}
// 3. 写入缓存
redisTemplate.opsForValue().set(key, source, 30, TimeUnit.MINUTES);
return source;
}
索引设计原则:
慢SQL优化案例:
sql复制-- 优化前
SELECT * FROM registration WHERE patient_id = 123 AND status = 1 ORDER BY create_time DESC;
-- 优化后
ALTER TABLE registration ADD INDEX idx_patient_status_time (patient_id, status, create_time DESC);
挂号场景的并发控制:
java复制@Update("UPDATE registration_source SET remaining = remaining - 1, version = version + 1
WHERE id = #{id} AND version = #{version}")
int deductRemainingWithVersion(@Param("id") Long id, @Param("version") Integer version);
lua复制if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then
return redis.call('expire', KEYS[1], ARGV[2])
else
return 0
end
采用JWT + Spring Security实现:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/patient/**").hasRole("PATIENT")
.antMatchers("/api/doctor/**").hasRole("DOCTOR")
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()));
return http.build();
}
}
患者敏感信息处理:
java复制public class PatientInfoDesensitizer {
public static String desensitizeIdCard(String idCard) {
if (StringUtils.isBlank(idCard)) {
return "";
}
return idCard.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1****$2");
}
public static String desensitizePhone(String phone) {
if (StringUtils.isBlank(phone)) {
return "";
}
return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}
}
关键操作日志记录:
java复制@Aspect
@Component
@Slf4j
public class OperationLogAspect {
@Pointcut("@annotation(com.example.hospital.annotation.OperationLog)")
public void operationLogPointCut() {}
@Around("operationLogPointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
OperationLog annotation = method.getAnnotation(OperationLog.class);
long beginTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
long time = System.currentTimeMillis() - beginTime;
logOperation(joinPoint, annotation, time, result);
return result;
}
}
Docker Compose配置示例:
yaml复制version: '3'
services:
app:
image: hospital-registration:1.0.0
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
depends_on:
- redis
- mysql
redis:
image: redis:6.2
ports:
- "6379:6379"
volumes:
- redis_data:/data
mysql:
image: mysql:8.0
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=root
volumes:
- mysql_data:/var/lib/mysql
volumes:
redis_data:
mysql_data:
Prometheus + Grafana监控指标:
SpringBoot Actuator配置:
yaml复制management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
metrics:
enabled: true
prometheus:
enabled: true
在实际开发过程中,我们遇到了几个关键挑战和解决方案:
高并发挂号场景:最初直接操作数据库导致性能瓶颈,后来引入Redis缓存号源信息+分布式锁方案,QPS从200提升到5000。
数据一致性:支付成功但挂号失败的情况,通过本地消息表+定时任务实现最终一致性。
老系统对接:与医院HIS系统对接时,采用中间表+增量同步策略,确保数据实时性。
给开发者的几点建议:
这个项目让我深刻体会到,一个好的医疗系统不仅需要技术实力,更需要深入理解医疗业务流程。我们在开发过程中与医护人员密切合作,经过3个版本的迭代才最终达到理想效果。