1. 项目背景与核心需求
中山社区医疗综合服务平台是一个面向社区居民的医疗健康服务系统,旨在整合区域内医疗资源,提供便捷的线上医疗服务。项目采用SpringBoot作为基础框架,主要解决以下核心问题:
- 医疗资源碎片化:社区医院、诊所、药房等机构信息分散,居民获取不便
- 就诊流程繁琐:传统挂号、缴费、取药等环节排队时间长
- 健康管理缺失:居民缺乏系统化的健康档案和跟踪服务
- 应急响应滞后:突发医疗需求无法快速对接合适资源
SpringBoot的自动配置和快速开发特性,特别适合此类需要快速迭代的社区服务系统。其内嵌Tomcat服务器和约定优于配置的原则,让开发团队能专注于业务逻辑而非环境搭建。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术架构设计
2.1 整体架构分层
平台采用经典三层架构,各层技术选型如下:
| 层级 | 组件 | 技术选型 | 选型理由 |
|---|---|---|---|
| 表现层 | Web前端 | Vue.js + ElementUI | 组件化开发,社区医疗场景需要丰富的表单交互 |
| 业务层 | 后端服务 | SpringBoot 2.7 + SpringMVC | 快速构建RESTful API,简化HTTP请求处理 |
| 数据层 | 持久化 | MyBatis-Plus + MySQL | 兼顾灵活性与开发效率,自带CRUD操作简化 |
| 中间件 | 消息队列 | RabbitMQ | 处理预约挂号、药品库存等异步消息 |
| 缓存 | 数据缓存 | Redis | 高频访问的医生排班、药品目录缓存 |
2.2 核心模块划分
java复制com.zsmed
├── common // 公共组件
├── config // 配置类
├── controller // 控制层
│ ├── AppointmentController.java // 预约挂号
│ ├── MedicalRecordController.java // 电子病历
│ └── PharmacyController.java // 药品管理
├── service // 业务层
│ ├── impl // 实现类
│ └── ...
├── mapper // 数据访问
└── entity // 实体类
3. 关键功能实现
3.1 多数据源动态切换
社区医疗系统需要同时对接:
- 核心业务数据库(MySQL)
- 医保结算系统(Oracle)
- 公共卫生平台(PostgreSQL)
通过AbstractRoutingDataSource实现动态切换:
java复制public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}
// 使用注解切换数据源
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
String value() default "master";
}
避坑指南:
- MyBatis缓存会导致切换失效,需配置
flushCache="true" - 事务方法内切换无效,需在事务开启前指定数据源
- 连接池需单独配置,避免不同数据库驱动冲突
3.2 电子病历PDF导出
结合Apache POI和iText实现病历导出:
java复制public void exportMedicalRecord(Long recordId, HttpServletResponse response) {
MedicalRecord record = recordService.getById(recordId);
// 1. 使用Freemarker生成HTML模板
Configuration cfg = new Configuration(Configuration.VERSION_2_3_30);
String html = FreeMarkerTemplateUtils.processTemplate("record.ftl", record);
// 2. 转换为PDF
PdfWriter writer = PdfWriter.getInstance(document, response.getOutputStream());
XMLWorkerHelper.getInstance().parseXHtml(writer, document,
new StringReader(html));
// 3. 设置响应头
response.setContentType("application/pdf");
response.setHeader("Content-Disposition",
"attachment;filename=record_" + recordId + ".pdf");
}
性能优化点:
- 预编译FreeMarker模板
- 使用线程池处理批量导出
- PDF生成添加进度监控
4. 安全与权限控制
4.1 细粒度数据权限
不同角色可见数据范围不同:
- 居民:仅个人及家属数据
- 社区医生:管辖社区居民数据
- 管理员:全社区数据
通过MyBatis拦截器实现:
java复制@Intercepts(@Signature(type= Executor.class, method="query",
args={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class DataPermissionInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取当前用户角色
User user = SecurityUtils.getCurrentUser();
// 修改SQL添加数据过滤条件
if (user.isResident()) {
String sql = boundSql.getSql();
sql = sql + " AND patient_id = " + user.getId();
resetSql(invocation, sql);
}
return invocation.proceed();
}
}
4.2 敏感数据脱敏
医疗数据需符合《个人信息保护法》要求:
java复制public class DataMaskingSerializer extends JsonSerializer<String> {
@Override
public void serialize(String value, JsonGenerator gen,
SerializerProvider provider) throws IOException {
if (StringUtils.isBlank(value)) {
gen.writeString("");
return;
}
// 身份证号脱敏
if (value.matches("\\d{17}[\\dXx]")) {
gen.writeString(value.replaceAll("(\\d{3})\\d{11}(\\w{4})", "$1****$2"));
}
// 手机号脱敏
else if (value.matches("1\\d{10}")) {
gen.writeString(value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"));
}
}
}
5. 运维部署方案
5.1 Docker容器化部署
dockerfile复制FROM openjdk:11-jre
WORKDIR /app
COPY target/community-medical-*.jar app.jar
EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java","-jar","app.jar"]
生产环境建议:
- JVM参数调优:
bash复制
-XX:MaxRAMPercentage=75.0 -XX:+UseG1GC -XX:MaxGCPauseMillis=200 - 日志收集:
yaml复制logging: file: path: /var/log/med logback: rollingpolicy: max-file-size: 50MB
5.2 监控告警配置
SpringBoot Actuator + Prometheus方案:
yaml复制management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
export:
prometheus:
enabled: true
tags:
application: ${spring.application.name}
关键监控指标:
- 接口响应时间(http_server_requests_seconds)
- JVM内存使用(jvm_memory_used_bytes)
- 数据库连接池(hikaricp_connections_active)
6. 典型问题解决方案
6.1 高并发预约冲突
采用Redis分布式锁防止超卖:
java复制public boolean makeAppointment(Long scheduleId) {
String lockKey = "lock:appointment:" + scheduleId;
String requestId = UUID.randomUUID().toString();
try {
// 获取锁(SETNX)
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
// 查询剩余号源
Integer remain = scheduleMapper.selectRemain(scheduleId);
if (remain > 0) {
// 扣减库存
scheduleMapper.updateRemain(scheduleId, remain - 1);
return true;
}
}
return false;
} finally {
// 释放锁(Lua脚本保证原子性)
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey), requestId);
}
}
6.2 大数据量导出OOM
采用分页查询+流式写入:
java复制public void exportAllRecords(HttpServletResponse response) {
response.setContentType("application/octet-stream");
try (Workbook workbook = new SXSSFWorkbook(100)) {
Sheet sheet = workbook.createSheet("病历记录");
// 分批查询(每次500条)
int pageSize = 500;
for (int page = 1; ; page++) {
Page<MedicalRecord> records = recordService.page(
new Page<>(page, pageSize));
if (records.getRecords().isEmpty()) break;
// 写入当前批次
for (MedicalRecord record : records.getRecords()) {
Row row = sheet.createRow(sheet.getLastRowNum() + 1);
row.createCell(0).setCellValue(record.getPatientName());
// 其他字段...
}
}
workbook.write(response.getOutputStream());
}
}
7. 项目演进方向
- 智能分诊:集成NLP引擎分析症状描述,推荐合适科室
- 药品库存预警:基于历史数据预测药品消耗,自动生成采购单
- 健康画像:通过居民就诊记录构建健康风险模型
- 物联网集成:对接智能穿戴设备实时监测慢性病患者体征
实际开发中发现,社区医疗场景对系统可靠性要求极高,但同时又需要快速响应政策变化。SpringBoot的"约定优于配置"理念大幅减少了样板代码,让团队能聚焦业务创新。特别在疫情期间,仅用3天就完成了疫苗接种预约模块的紧急上线。
