1. 项目概述
作为一名在医院信息化领域摸爬滚打多年的开发者,我深知传统纸质医疗管理的痛点。最近刚完成了一个基于Spring Boot的医院管理系统,这套系统从需求调研到最终上线历时6个月,期间踩过不少坑也积累了不少经验。今天就把这个项目的完整实现思路和技术细节分享给大家,特别是数据库设计和Spring Boot的实战技巧部分,相信对正在开发类似系统的同行会有很大帮助。
这个系统主要解决了医院日常管理中的四大核心问题:
- 患者信息电子化:从入院登记到病历管理全程无纸化
- 医疗资源数字化:病床、药品、科室等资源状态实时可视化
- 业务流程标准化:挂号、问诊、开药、住院等流程线上化
- 数据统计智能化:自动生成各类医疗业务报表
系统采用B/S架构,前端使用Vue+Element UI,后端基于Spring Boot 2.7 + MyBatis Plus,数据库选用MySQL 8.0。特别在数据库设计上,我们针对医疗行业的特殊性做了很多优化,比如病人信息的敏感字段加密、药品批次的双重校验等,这些都会在后文详细说明。
2. 核心需求与架构设计
2.1 角色与功能划分
系统设计了四类核心角色,每类角色的功能权限经过了三轮业务调研才最终确定:
管理员:
- 科室管理(增删改查+排班设置)
- 人员管理(医生/护士账号的权限分配)
- 系统监控(操作日志审计+异常报警)
- 数据备份(定时任务+手动触发)
医生:
- 电子病历(结构化录入模板)
- 处方开具(药品配伍禁忌提醒)
- 患者管理(历史就诊记录联动)
- 排班查询(可视化日历展示)
护士:
- 床位管理(状态实时更新)
- 医嘱执行(扫码核对机制)
- 护理记录(模板化快速录入)
- 药品申领(库存预警提示)
患者:
- 预约挂号(分时段号源管理)
- 报告查询(检验结果自动推送)
- 费用明细(实时扣费记录)
- 满意度评价(匿名反馈机制)
2.2 技术架构详解
系统采用经典的三层架构,但在细节上做了很多优化:
表现层:
- 前后端分离设计,通过JWT进行认证
- 接口文档使用Swagger 3.0自动生成
- 文件上传采用阿里云OSS存储
业务层:
- Spring Boot 2.7.12(稳定版)
- 自定义注解实现业务日志
- 分布式锁控制药品库存
- 规则引擎处理医疗计价
数据层:
- MySQL 8.0(RR隔离级别)
- 热数据使用Redis缓存
- 敏感字段AES加密存储
- 定时任务进行数据归档
特别说明一下我们的缓存设计策略:对于科室信息、药品目录等变化频率低的数据,采用Redis缓存24小时;对于床位状态、药品库存等高频变化数据,设置60秒短缓存保证数据及时性。
3. 数据库设计实战
3.1 核心表结构设计
医疗系统的数据库设计有几个特殊要求:字段多、关联复杂、历史数据需完整保留。我们的ER图经过12次迭代才最终定型,以下是几个关键表的设计要点:
病人信息表(patient)优化:
sql复制CREATE TABLE `patient` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`medical_record_no` varchar(32) COLLATE utf8mb4_bin NOT NULL COMMENT '病历号',
`name` varchar(50) COLLATE utf8mb4_bin NOT NULL COMMENT '姓名',
`id_card` varchar(18) COLLATE utf8mb4_bin NOT NULL COMMENT '身份证',
`id_card_encrypt` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '加密身份证',
`phone` varchar(11) COLLATE utf8mb4_bin NOT NULL COMMENT '手机号',
`blood_type` enum('A','B','AB','O') COLLATE utf8mb4_bin DEFAULT NULL COMMENT '血型',
`allergy_history` text COLLATE utf8mb4_bin COMMENT '过敏史',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_mr_no` (`medical_record_no`),
KEY `idx_id_card` (`id_card`),
KEY `idx_phone` (`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='病人信息';
设计亮点:
- 身份证号等敏感字段增加加密存储列
- 病历号采用独立唯一索引
- 血型使用ENUM类型约束取值范围
- 自动维护创建和更新时间
3.2 药品库存管理设计
药房管理最核心的就是库存准确性,我们设计了双重校验机制:
sql复制CREATE TABLE `medicine` (
`id` bigint NOT NULL AUTO_INCREMENT,
`code` varchar(20) NOT NULL COMMENT '药品编码',
`name` varchar(100) NOT NULL COMMENT '通用名',
`spec` varchar(50) NOT NULL COMMENT '规格',
`batch_no` varchar(30) NOT NULL COMMENT '批次号',
`produce_date` date NOT NULL COMMENT '生产日期',
`expire_date` date NOT NULL COMMENT '有效期',
`stock` int NOT NULL DEFAULT '0' COMMENT '当前库存',
`lock_stock` int NOT NULL DEFAULT '0' COMMENT '预扣库存',
`unit` varchar(10) NOT NULL COMMENT '单位',
`price` decimal(10,2) NOT NULL COMMENT '单价',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code_batch` (`code`,`batch_no`),
KEY `idx_expire` (`expire_date`)
) ENGINE=InnoDB COMMENT='药品库存';
CREATE TABLE `medicine_stock_log` (
`id` bigint NOT NULL AUTO_INCREMENT,
`medicine_id` bigint NOT NULL,
`change_amount` int NOT NULL COMMENT '变更数量',
`remaining` int NOT NULL COMMENT '变更后库存',
`operation_type` tinyint NOT NULL COMMENT '1入库 2出库 3报损',
`operator` varchar(50) NOT NULL,
`operation_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`biz_no` varchar(32) DEFAULT NULL COMMENT '关联业务单号',
PRIMARY KEY (`id`),
KEY `idx_medicine` (`medicine_id`),
KEY `idx_time` (`operation_time`)
) ENGINE=InnoDB COMMENT='库存流水';
关键设计:
- 药品编码+批次号唯一约束
- 采用预扣库存模式防止超卖
- 所有库存变更记录完整流水
- 建立有效期索引便于临期预警
4. Spring Boot后端实现
4.1 分层架构实践
我们的代码严格遵循DDD分层原则:
code复制com.hospital
├── application # 应用层
│ ├── dto # 数据传输对象
│ └── service # 应用服务
├── domain # 领域层
│ ├── model # 领域模型
│ └── repository # 仓储接口
├── infrastructure # 基础设施层
│ ├── dao # 持久化实现
│ └── util # 通用工具
└── interfaces # 接口层
├── web # Web接口
└── rpc # RPC接口
以处方开具为例的典型调用链路:
code复制PrescriptionController
→ PrescriptionAppService
→ PrescriptionDomainService
→ MedicineDomainService
→ PrescriptionRepository
4.2 关键代码实现
药品库存扣减的分布式锁实现:
java复制@Transactional(rollbackFor = Exception.class)
public void deductStock(Long medicineId, int quantity) {
String lockKey = "med:lock:" + medicineId;
// 使用Redis分布式锁,避免超卖
boolean locked = false;
try {
locked = redisLock.tryLock(lockKey, 10, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("系统繁忙,请稍后再试");
}
Medicine medicine = medicineRepository.findById(medicineId)
.orElseThrow(() -> new NotFoundException("药品不存在"));
if (medicine.getStock() - medicine.getLockStock() < quantity) {
throw new BusinessException("药品库存不足");
}
// 预扣库存
medicineRepository.updateLockStock(medicineId, quantity);
// 记录库存流水
MedicineStockLog log = new MedicineStockLog();
log.setMedicineId(medicineId);
log.setChangeAmount(-quantity);
log.setRemaining(medicine.getStock() - quantity);
log.setOperationType(StockOperationType.OUTBOUND);
stockLogRepository.save(log);
} finally {
if (locked) {
redisLock.unlock(lockKey);
}
}
}
使用Spring Event实现业务解耦:
java复制// 定义处方创建事件
public class PrescriptionCreatedEvent extends ApplicationEvent {
private Long prescriptionId;
public PrescriptionCreatedEvent(Object source, Long prescriptionId) {
super(source);
this.prescriptionId = prescriptionId;
}
// getter...
}
// 事件处理器
@Component
@Slf4j
public class PrescriptionEventHandler {
@Autowired
private NotificationService notificationService;
@Async
@EventListener
public void handlePrescriptionCreated(PrescriptionCreatedEvent event) {
// 异步发送通知
notificationService.sendPrescriptionNotice(event.getPrescriptionId());
}
}
// 业务代码中发布事件
public void createPrescription(PrescriptionDTO dto) {
// ...保存处方逻辑
applicationContext.publishEvent(new PrescriptionCreatedEvent(this, prescription.getId()));
}
5. 系统安全设计
5.1 医疗数据安全措施
- 敏感数据加密:
java复制// 使用AES加密身份证号
public class IdCardEncryptor {
private static final String KEY = "secureKey12345678"; // 实际应从配置中心获取
public static String encrypt(String idCard) {
try {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(KEY.getBytes(), "AES"));
return Base64.getEncoder().encodeToString(cipher.doFinal(idCard.getBytes()));
} catch (Exception e) {
throw new RuntimeException("加密失败", e);
}
}
}
- 操作日志审计:
- 使用Spring AOP记录所有数据修改操作
- 关键业务表增加version字段实现乐观锁
- 数据库变更通过Trigger记录历史版本
5.2 接口安全防护
- 防SQL注入:
- 严格使用MyBatis参数绑定
- 启用mybatis-filter防止恶意SQL
- 接口限流:
java复制@RestController
@RequestMapping("/api")
@Slf4j
public class PatientController {
@RateLimiter(value = 100, key = "'patient_query_' + #root.args[0]")
@GetMapping("/patients/{id}")
public PatientDTO getPatient(@PathVariable Long id) {
// 查询逻辑
}
}
6. 部署与性能优化
6.1 生产环境部署方案
我们最终采用的部署架构:
code复制 +-----------------+
| 阿里云SLB |
+--------+--------+
|
+----------------+-----------------+
| |
+----------+----------+ +----------+----------+
| Nginx(2C4G) | | Nginx(2C4G) |
+----------+----------+ +----------+----------+
| |
+----------+----------+ +----------+----------+
| App1(4C8G) | | App2(4C8G) |
| Spring Boot | | Spring Boot |
+----------+----------+ +----------+----------+
| |
+----------+----------+ +----------+----------+
| Redis哨兵 | | MySQL主从 |
| (1主2从) | | (1主2从) |
+-------------------+ +-------------------+
关键配置参数:
yaml复制# application-prod.yml
server:
tomcat:
max-threads: 200
min-spare-threads: 20
spring:
datasource:
hikari:
maximum-pool-size: 30
connection-timeout: 30000
redis:
lettuce:
pool:
max-active: 50
max-wait: 1000
6.2 性能优化实践
- SQL优化案例:
java复制// 优化前的N+1查询问题
public List<PrescriptionDTO> getByPatient(Long patientId) {
List<Prescription> prescriptions = prescriptionRepo.findByPatientId(patientId);
return prescriptions.stream().map(p -> {
PrescriptionDTO dto = convert(p);
// 每次循环都查询药品明细
dto.setMedicines(medicineRepo.findByPrescriptionId(p.getId()));
return dto;
}).collect(Collectors.toList());
}
// 优化后使用JOIN一次查询
@Query("SELECT new com.hospital.application.dto.PrescriptionDetailDTO(" +
"p.id, p.patientName, m.name, m.spec, pm.quantity) " +
"FROM Prescription p " +
"JOIN PrescriptionMedicine pm ON p.id = pm.prescriptionId " +
"JOIN Medicine m ON pm.medicineId = m.id " +
"WHERE p.patientId = :patientId")
List<PrescriptionDetailDTO> findDetailByPatient(@Param("patientId") Long patientId);
- 缓存使用技巧:
- 药品目录使用Guava Cache本地缓存
- 患者基本信息使用Redis缓存并设置不同过期时间
- 采用Cache Aside Pattern避免缓存一致性问题
7. 踩坑与经验总结
7.1 典型问题排查
问题1:批量插入药品数据时性能极差
现象:导入1000条药品数据需要3分钟
排查:
- 发现JDBC连接池waiting线程堆积
- 检查SQL日志发现是单条INSERT
- 确认MyBatis批量配置未生效
解决:
yaml复制# 添加MyBatis配置
mybatis:
executor-type: batch
问题2:患者查询接口偶尔返回旧数据
现象:修改患者信息后,查询有时还是返回旧数据
排查:
- 确认数据库数据已更新
- 检查Redis缓存TTL设置正常
- 发现是Nginx配置了10秒静态缓存
解决:
nginx复制location /api/patients/ {
proxy_cache off;
proxy_pass http://backend;
}
7.2 经验总结
- 医疗业务特殊性:
- 所有修改操作必须保留完整操作日志
- 关键业务数据需要双重确认机制
- 界面设计要考虑医护人员操作习惯
- 技术选型建议:
- 医疗系统优先考虑稳定性而非新技术
- 数据库设计要预留足够的扩展字段
- 接口设计要考虑未来HIS系统对接
- 团队协作心得:
- 医疗术语需要建立统一数据字典
- 复杂业务流程要有状态机图辅助理解
- 与医护人员保持定期需求确认
这个项目让我深刻体会到医疗信息化建设的复杂性,每一个设计决策都可能影响实际诊疗流程。比如我们最初设计的药品库存扣减是实时扣减,但在实际使用中医生经常需要修改处方,后来改为预扣库存+最终确认的两阶段模式,这才符合实际工作场景。