1. 项目概述
最近完成了一个基于SpringBoot+Vue的疫苗接种预约平台项目,这个系统主要解决了疫情期间疫苗接种预约难、管理混乱的问题。作为一个全栈项目,后端采用SpringBoot框架,前端使用Vue.js,通过RESTful API实现前后端分离。在实际开发过程中,遇到了不少技术挑战,也积累了一些值得分享的经验。
这个系统最核心的价值在于:
- 实现了疫苗预约的数字化管理,避免了线下排队带来的交叉感染风险
- 通过分时段预约机制,有效分散了接种点的人流压力
- 为管理部门提供了实时的数据看板,便于掌握接种进度和疫苗库存情况
2. 技术架构设计
2.1 整体架构方案
我们采用了经典的三层架构:
- 表现层:Vue.js + Element UI
- 业务逻辑层:SpringBoot + MyBatis-Plus
- 数据持久层:MySQL
前后端完全分离,通过RESTful API进行通信。这种架构的优势在于:
- 前后端可以并行开发,提高开发效率
- 前端可以灵活替换,不影响后端业务逻辑
- 更适合应对高并发场景
2.2 关键技术选型
后端技术栈:
- SpringBoot 2.7.x:简化了Spring应用的初始搭建和开发过程
- MyBatis-Plus 3.5.x:强大的ORM框架,提供了丰富的CRUD接口
- Redis 6.x:用于分布式锁和缓存
- RabbitMQ 3.9.x:异步消息处理
前端技术栈:
- Vue.js 3.x:响应式前端框架
- Element Plus:UI组件库
- Axios:HTTP客户端
- ECharts:数据可视化
提示:在实际项目中,建议锁定这些依赖的具体版本号,避免因版本升级带来的兼容性问题。
3. 核心功能实现
3.1 预约管理模块
这是系统的核心功能,主要包含以下子功能:
- 接种点选择:基于地理位置推荐附近的接种点
- 分时段预约:将每天划分为多个时段,控制每个时段的预约人数
- 疫苗类型筛选:支持多种疫苗的预约(如新冠疫苗、HPV疫苗等)
关键技术实现:
java复制// 预约接口核心逻辑
@Transactional
public AppointmentResult makeAppointment(AppointmentRequest request) {
// 1. 获取分布式锁
String lockKey = "appoint_lock:" + request.getTimeSlotId();
try {
boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("当前时段预约人数过多,请稍后再试");
}
// 2. 检查库存
VaccineStock stock = vaccineStockMapper.selectById(request.getVaccineId());
if (stock.getAvailable() <= 0) {
throw new BusinessException("该疫苗库存不足");
}
// 3. 创建预约记录
Appointment appointment = new Appointment();
// 设置各种属性...
appointmentMapper.insert(appointment);
// 4. 扣减库存
vaccineStockMapper.updateAvailable(request.getVaccineId(), -1);
// 5. 发送通知
rabbitTemplate.convertAndSend("appointment.notification",
new NotificationMessage(appointment));
return new AppointmentResult(true, "预约成功");
} finally {
// 释放锁
redisTemplate.delete(lockKey);
}
}
3.2 数据看板模块
为管理人员提供可视化数据展示:
- 接种统计:按地区、时间段统计接种人数
- 预约趋势:展示预约量的变化趋势
- 库存预警:当库存低于阈值时发出预警
前端使用ECharts实现数据可视化:
javascript复制// 预约趋势图示例
const initTrendChart = () => {
const chartDom = document.getElementById('trend-chart');
const myChart = echarts.init(chartDom);
axios.get('/api/dashboard/trend').then(response => {
const option = {
xAxis: {
type: 'category',
data: response.data.dates
},
yAxis: {
type: 'value'
},
series: [{
data: response.data.counts,
type: 'line',
smooth: true
}]
};
myChart.setOption(option);
});
}
3.3 权限控制模块
采用RBAC(基于角色的访问控制)模型,定义了三种角色:
- 管理员:拥有全部权限
- 医护人员:可以查看和确认接种记录
- 普通用户:只能进行预约和个人信息管理
权限验证使用Spring Security + JWT实现:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.antMatchers("/api/doctor/**").hasRole("DOCTOR")
.antMatchers("/api/user/**").hasRole("USER")
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()));
}
}
4. 数据库设计
4.1 主要表结构
用户表(user):
sql复制CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(100) NOT NULL COMMENT '密码',
`real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名',
`id_card` varchar(18) DEFAULT NULL COMMENT '身份证号',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
`role` enum('ADMIN','DOCTOR','USER') NOT NULL DEFAULT 'USER' COMMENT '角色',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_username` (`username`),
KEY `idx_phone` (`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
预约表(appointment):
sql复制CREATE TABLE `appointment` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL COMMENT '用户ID',
`vaccine_id` bigint NOT NULL COMMENT '疫苗ID',
`site_id` bigint NOT NULL COMMENT '接种点ID',
`time_slot` datetime NOT NULL COMMENT '预约时段',
`status` enum('PENDING','CONFIRMED','CANCELLED','COMPLETED') NOT NULL DEFAULT 'PENDING' COMMENT '状态',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_user` (`user_id`),
KEY `idx_time_slot` (`time_slot`),
KEY `idx_site_time` (`site_id`,`time_slot`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
4.2 索引优化建议
- 为高频查询条件创建索引,如预约表的
(site_id, time_slot)联合索引 - 避免过度索引,每个额外的索引都会降低写入性能
- 定期使用EXPLAIN分析慢查询,优化SQL语句
5. 高并发处理
5.1 分布式锁实现
预约时段存在秒杀场景,使用Redis实现分布式锁:
java复制public boolean tryLock(String key, long expireSeconds) {
String value = UUID.randomUUID().toString();
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(key, value, expireSeconds, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
}
public void unlock(String key) {
// 使用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(key),
redisTemplate.opsForValue().get(key));
}
5.2 消息队列应用
使用RabbitMQ异步处理:
- 预约成功通知
- 短信提醒
- 数据同步
配置示例:
java复制@Configuration
public class RabbitConfig {
@Bean
public Queue appointmentQueue() {
return new Queue("appointment.notification");
}
@Bean
public Jackson2JsonMessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
}
6. 安全措施
6.1 认证与授权
- 使用JWT进行无状态认证
- 密码采用BCrypt加密存储
- 敏感接口增加频率限制
JWT工具类示例:
java复制public class JwtUtil {
private static final String SECRET = "your-secret-key";
private static final long EXPIRATION = 86400000; // 24小时
public static String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
}
6.2 数据安全
- 敏感字段如身份证号进行加密存储
- 日志中脱敏处理敏感信息
- 定期备份数据库
7. 部署实践
7.1 后端部署
推荐使用Docker部署:
dockerfile复制FROM openjdk:11-jre
WORKDIR /app
COPY target/vaccine-system.jar .
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "vaccine-system.jar"]
7.2 前端部署
使用Nginx作为静态资源服务器:
nginx复制server {
listen 80;
server_name your.domain.com;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
8. 踩坑经验
-
分布式锁失效问题:最初没有设置锁的超时时间,导致系统崩溃后锁永远无法释放。解决方案是必须设置合理的超时时间,并考虑锁续期机制。
-
库存超卖问题:仅靠数据库乐观锁在高并发下仍会出现超卖。最终采用Redis分布式锁+数据库乐观锁双重保障。
-
JWT续签问题:JWT过期后用户需要重新登录体验不好。解决方案是前端在token快过期时自动调用续签接口。
-
Vue路由缓存问题:keep-alive导致某些页面状态异常。需要针对不同路由配置不同的缓存策略。
-
时间处理问题:前后端时区不一致导致预约时间显示错误。统一使用UTC时间传输,前端按用户时区显示。
9. 性能优化
- 缓存热点数据:将接种点信息、疫苗信息等不常变的数据放入Redis缓存
- 数据库读写分离:查询操作走从库,减轻主库压力
- 前端懒加载:大列表数据分页加载,减少一次性请求数据量
- CDN加速:静态资源部署到CDN,提高访问速度
性能对比:
| 优化措施 | QPS提升 | 平均响应时间降低 |
|---|---|---|
| 无缓存 | 基准 | 基准 |
| 添加Redis缓存 | 3.2倍 | 65% |
| 读写分离 | 1.8倍 | 40% |
| 全量优化 | 5.7倍 | 78% |
10. 扩展性设计
- 第三方对接:预留了健康码核验接口,支持与各地健康码平台对接
- 规则引擎:将预约规则配置化,支持不同疫苗的不同预约规则
- 插件机制:通过SPI机制支持功能扩展
未来可能的扩展方向:
- 增加微信小程序端
- 对接电子医保卡
- 支持团体预约功能
- 接入AI客服系统
这个项目从技术选型到最终上线历时3个月,期间遇到了不少挑战,但也收获了很多宝贵的经验。最大的体会是,一个好的系统不仅要有完善的功能,更需要考虑性能、安全性和可维护性。特别是在医疗健康领域,系统的稳定性和数据安全性更是重中之重。