1. 项目背景与核心需求
高校门诊作为校园医疗服务的重要载体,每天需要处理大量师生挂号、问诊、药品管理等事务。传统纸质登记方式存在信息孤岛、统计困难、药品管理混乱等问题。我们团队基于实际调研发现,一所万人规模的高校门诊部平均每天需要处理300-500人次挂号,每月药品出入库记录超过2000条,手工操作极易出错。
这个基于SpringBoot的门诊管理系统主要解决以下痛点:
- 挂号排队时间长:通过线上分时段预约,减少现场等待
- 药品管理混乱:实现批次号追踪和效期预警
- 数据统计困难:自动生成各类报表(就诊量、药品消耗等)
- 信息不互通:建立统一的师生健康档案
2. 技术架构设计
2.1 整体技术栈选型
后端技术矩阵:
- SpringBoot 2.7.x:简化配置,快速构建微服务
- MyBatis-Plus 3.5.x:增强型ORM框架
- JWT + Spring Security:认证与授权方案
- Redis 6.x:缓存热点数据(如药品目录)
- MySQL 8.0:主数据库
- Hutool 5.8.x:工具类库
前端技术方案:
- Vue 3.x + Composition API
- Element Plus组件库
- ECharts 5.x:数据可视化
- Axios:HTTP客户端
技术选型考量:高校IT环境通常存在服务器配置有限、运维力量薄弱的特点。SpringBoot内置Tomcat避免了中间件单独部署,Vue的渐进式特性适合功能模块的逐步迭代。MyBatis-Plus的代码生成器大幅减少了基础CRUD工作量。
2.2 系统架构图
code复制[用户层]
↓
[表现层] Vue前端 → Nginx反向代理
↓
[应用层] SpringBoot微服务集群
↓
[数据层] MySQL → Redis缓存
↓
[基础设施] 校园云服务器
关键设计原则:
- 前后端完全分离:通过RESTful API交互
- 无状态设计:JWT替代Session
- 读写分离:高频查询走缓存
- 弹性扩展:Docker容器化部署
3. 核心功能实现
3.1 挂号管理模块
数据库设计:
sql复制CREATE TABLE `registration` (
`id` bigint NOT NULL AUTO_INCREMENT,
`student_id` varchar(20) NOT NULL COMMENT '学工号',
`department_id` int NOT NULL COMMENT '科室ID',
`doctor_id` int DEFAULT NULL COMMENT '医生ID',
`register_time` datetime NOT NULL COMMENT '挂号时间',
`visit_date` date NOT NULL COMMENT '就诊日期',
`time_slot` tinyint NOT NULL COMMENT '时段(1-上午,2-下午)',
`status` tinyint DEFAULT '0' COMMENT '状态(0-待就诊,1-已就诊)',
PRIMARY KEY (`id`),
KEY `idx_student` (`student_id`),
KEY `idx_visit` (`visit_date`,`time_slot`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
并发控制方案:
采用乐观锁解决挂号时段冲突问题:
java复制@Transactional
public boolean register(RegistrationDTO dto) {
// 检查时段剩余名额
Long count = lambdaQuery()
.eq(Registration::getVisitDate, dto.getVisitDate())
.eq(Registration::getTimeSlot, dto.getTimeSlot())
.count();
if(count >= MAX_SLOT_CAPACITY) {
throw new BusinessException("该时段已约满");
}
// 构建实体
Registration entity = new Registration()
.setStudentId(dto.getStudentId())
.setDepartmentId(dto.getDepartmentId())
.setVisitDate(dto.getVisitDate())
.setTimeSlot(dto.getTimeSlot())
.setVersion(1); // 初始版本号
return save(entity);
}
3.2 药品管理模块
库存管理关键流程:
-
入库流程:
- 扫描药品条形码获取基本信息
- 校验药品批号、效期
- 生成入库单(包含:操作人、供应商、存储位置)
-
出库规则:
- 先进先出(FIFO)原则
- 效期预警(提前3个月标黄,1个月标红)
- 最低库存阈值提醒
库存扣减SQL示例:
sql复制UPDATE medicine_stock
SET stock = stock - #{amount},
version = version + 1
WHERE medicine_id = #{medicineId}
AND batch_no = #{batchNo}
AND stock >= #{amount}
AND version = #{version}
3.3 安全控制实现
JWT增强方案:
java复制public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken,
OAuth2Authentication authentication) {
Map<String, Object> info = new HashMap<>();
// 添加自定义claim
User user = (User) authentication.getPrincipal();
info.put("userId", user.getId());
info.put("role", user.getRole());
info.put("dept", user.getDepartment());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
return accessToken;
}
}
接口权限注解:
java复制@PreAuthorize("@permissionCheck.hasPermission('medicine:stock:out')")
@PostMapping("/out")
public R medicineOut(@RequestBody MedicineOutDTO dto) {
// 出库业务逻辑
}
4. 系统特色功能
4.1 智能排班算法
结合历史就诊数据自动生成医生排班:
java复制public List<Schedule> generateSchedule(LocalDate startDate, LocalDate endDate) {
// 1. 获取各科室历史就诊量权重
Map<Integer, Double> deptWeights = getHistoricalWeights();
// 2. 计算每日需求系数
Map<DayOfWeek, Double> dayFactors = Map.of(
DayOfWeek.MONDAY, 1.2,
DayOfWeek.FRIDAY, 1.1,
// ...其他星期系数
);
// 3. 遗传算法排班
return geneticAlgorithmScheduler.schedule(
startDate, endDate, deptWeights, dayFactors);
}
4.2 药品效期预警
定时任务配置:
java复制@Scheduled(cron = "0 0 8 * * ?") // 每天8点执行
public void checkMedicineExpiry() {
LocalDate warnDate = LocalDate.now().plusMonths(3);
List<MedicineStock> expiring = stockMapper.selectList(
lambdaQuery()
.le(MedicineStock::getExpiryDate, warnDate)
.eq(MedicineStock::getExpired, false)
);
expiring.forEach(item -> {
// 发送企业微信通知
wechatNotify.sendToPharmacist(
"药品效期预警:%s 批次%s 将于%s过期"
.formatted(item.getMedicineName(),
item.getBatchNo(),
item.getExpiryDate()));
});
}
5. 部署与性能优化
5.1 服务器配置建议
最低生产环境要求:
- CPU:4核(推荐8核)
- 内存:8GB(推荐16GB)
- 磁盘:100GB SSD(需单独挂载数据盘)
- 带宽:10Mbps(建议配置负载均衡)
5.2 关键性能参数
通过JMeter压测结果:
- 挂号接口:500并发下平均响应时间<200ms
- 药品查询:Redis缓存命中率>95%
- 批量导入:使用MyBatis批处理模式,1000条记录<3s
5.3 缓存策略示例
java复制@Cacheable(value = "medicine", key = "#name + '_' + #spec")
public Medicine getByNameAndSpec(String name, String spec) {
return lambdaQuery()
.eq(Medicine::getName, name)
.eq(Medicine::getSpecification, spec)
.one();
}
@CacheEvict(value = "medicine", allEntries = true)
public void updateMedicine(Medicine medicine) {
updateById(medicine);
}
6. 典型问题解决方案
6.1 挂号时段冲突
问题现象:
多用户同时预约同一时段导致超量
解决方案:
- 数据库唯一索引:
sql复制ALTER TABLE registration ADD UNIQUE INDEX udx_student_date (student_id, visit_date); - Redis分布式锁:
java复制public boolean tryRegister(String lockKey, long expireTime) { String token = UUID.randomUUID().toString(); try { Boolean acquired = redisTemplate.opsForValue() .setIfAbsent(lockKey, token, expireTime, TimeUnit.SECONDS); return Boolean.TRUE.equals(acquired); } catch (Exception e) { log.error("获取锁异常", e); return false; } }
6.2 药品库存扣减
常见错误:
超卖问题
最终方案:
java复制@Transactional
public boolean reduceStock(Long medicineId, String batchNo, int amount) {
// 1. 检查库存
MedicineStock stock = stockMapper.selectOne(
lambdaQuery()
.eq(MedicineStock::getMedicineId, medicineId)
.eq(MedicineStock::getBatchNo, batchNo)
.last("FOR UPDATE") // 悲观锁
);
if(stock == null || stock.getStock() < amount) {
return false;
}
// 2. 扣减库存
return stockMapper.updateStock(
medicineId, batchNo, amount, stock.getVersion()) > 0;
}
7. 项目演进方向
- 移动端扩展:对接企业微信/钉钉,实现移动挂号
- 智能诊断辅助:集成NLP引擎分析病史描述
- 物联网集成:连接智能药柜实现自动盘点
- 大数据分析:构建师生健康画像
实际开发中我们发现,门诊业务流程存在大量可优化的细节。比如在药品入库环节,通过OCR识别药品说明书关键信息;在挂号环节,根据症状描述智能推荐科室等。这些改进点都记录在我们的迭代清单中。