1. 项目背景与核心需求分析
宠物医疗行业近年来呈现爆发式增长,根据2023年宠物医疗行业白皮书显示,全国宠物医院数量已突破3万家,年就诊量增长率保持在25%以上。在这样的市场背景下,传统电话预约方式暴露出诸多痛点:高峰时段占线严重、预约信息记录易出错、医生排班难以可视化等。这正是我们开发"猫狗宠物领养医院预约展示系统"的核心驱动力。
这个系统需要同时满足三类用户的差异化需求:
- 宠物主人:能够实时查看医生排班、在线预约特定时段、查询历史就诊记录
- 医院管理员:需要管理医生信息、处理预约冲突、生成各类统计报表
- 前台接待人员:快速办理现场登记、处理预约变更、打印就诊单据
技术选型上采用SpringBoot+Vue3的组合,主要基于以下考量:
- SpringBoot的自动配置特性可快速搭建医院业务微服务
- Vue3的Composition API更适合处理复杂的预约状态管理
- 前后端分离架构便于后续扩展移动端应用
- 宠物医院的特殊业务场景(如急诊插队处理)需要灵活的技术架构支持
2. 系统架构设计
2.1 技术栈全景图
本系统采用经典的三层架构,具体技术组件如下:
code复制前端层:
- Vue 3.2 + Composition API
- Element Plus组件库
- Axios HTTP客户端
- Vue Router 4.x
- Pinia状态管理
后端层:
- Spring Boot 2.7.x
- Spring Security + JWT
- MyBatis-Plus 3.5.x
- Redis 6.x缓存
- Quartz定时任务
数据层:
- MySQL 8.0关系型数据库
- MongoDB 5.0(存储非结构化诊疗记录)
- MinIO对象存储(宠物影像资料)
2.2 核心业务流程设计
预约业务的状态机设计尤为关键,我们定义了7种核心状态:
mermaid复制stateDiagram-v2
[*] --> 待支付
待支付 --> 已预约: 支付成功
已预约 --> 就诊中: 签到
就诊中 --> 已完成: 医生确认
已预约 --> 已取消: 用户取消
已预约 --> 已过期: 超时未签到
就诊中 --> 已退款: 特殊情况
特别注意:实际开发中需要处理并发的状态变更问题,建议采用乐观锁机制。我们在初期版本就遇到过多个终端同时修改状态导致数据不一致的情况。
2.3 数据库关键表结构
2.3.1 医生排班表设计
sql复制CREATE TABLE `schedule` (
`id` bigint NOT NULL AUTO_INCREMENT,
`doctor_id` bigint NOT NULL COMMENT '关联医生ID',
`clinic_id` int NOT NULL COMMENT '诊室编号',
`start_time` datetime NOT NULL COMMENT '排班开始时间',
`end_time` datetime NOT NULL COMMENT '排班结束时间',
`max_appointments` int DEFAULT '15' COMMENT '最大预约数',
`current_appointments` int DEFAULT '0' COMMENT '当前预约数',
`status` tinyint DEFAULT '1' COMMENT '0-停诊 1-正常',
`version` int DEFAULT '0' COMMENT '乐观锁版本号',
PRIMARY KEY (`id`),
KEY `idx_doctor_time` (`doctor_id`,`start_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2.3.2 预约订单表设计
sql复制CREATE TABLE `appointment` (
`id` bigint NOT NULL AUTO_INCREMENT,
`order_no` varchar(32) NOT NULL COMMENT '预约单号',
`pet_id` bigint NOT NULL COMMENT '宠物ID',
`user_id` bigint NOT NULL COMMENT '用户ID',
`schedule_id` bigint NOT NULL COMMENT '关联排班ID',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '0-待支付 1-已预约 2-就诊中 3-已完成 4-已取消',
`symptoms` varchar(500) DEFAULT NULL COMMENT '症状描述',
`amount` decimal(10,2) DEFAULT NULL 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_order_no` (`order_no`),
KEY `idx_user_id` (`user_id`),
KEY `idx_schedule_id` (`schedule_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3. 关键功能实现细节
3.1 预约冲突处理机制
宠物医院的特殊性在于经常遇到急诊插队的情况,我们设计了多级优先级的预约处理策略:
- 常规预约:普通用户提前预约,遵循FIFO原则
- VIP预约:会员用户可预约热门时段
- 急诊预约:由前台强制插入,自动顺延后续预约
- 复诊预约:医生直接为患者安排下次就诊时间
实现代码示例(SpringBoot侧):
java复制@Transactional
public AppointmentResult createAppointment(AppointmentDTO dto) {
// 1. 校验排班信息
Schedule schedule = scheduleMapper.selectById(dto.getScheduleId());
if (schedule == null || schedule.getStatus() == 0) {
throw new BusinessException("该时段不可预约");
}
// 2. 乐观锁控制并发
int updated = scheduleMapper.updateCurrentAppointments(
schedule.getId(),
schedule.getVersion(),
schedule.getVersion() + 1);
if (updated == 0) {
throw new ConcurrentBookingException("当前时段预约冲突,请重试");
}
// 3. 创建预约记录
Appointment appointment = new Appointment();
BeanUtils.copyProperties(dto, appointment);
appointment.setOrderNo(generateOrderNo());
appointmentMapper.insert(appointment);
// 4. 返回结果
return new AppointmentResult(appointment.getOrderNo(), schedule.getStartTime());
}
3.2 实时座位展示实现
前端使用Vue3的Composition API实现诊室座位动态渲染:
vue复制<template>
<div class="clinic-map">
<div
v-for="seat in seats"
:key="seat.id"
:class="['seat', seat.status]"
@click="handleSeatClick(seat)"
>
<span class="seat-number">{{ seat.number }}</span>
<div class="seat-info">
<span v-if="seat.petName">{{ seat.petName }}</span>
<span v-else>可预约</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useWebSocket } from '@/composables/useWebSocket'
const seats = ref([])
const { initSocket, closeSocket } = useWebSocket()
onMounted(async () => {
const response = await fetch('/api/seats')
seats.value = await response.json()
initSocket('seat-update', (data) => {
const index = seats.value.findIndex(s => s.id === data.id)
if (index !== -1) {
seats.value[index] = { ...seats.value[index], ...data }
}
})
})
onUnmounted(() => {
closeSocket()
})
</script>
4. 特殊业务场景处理
4.1 疫苗预约的特殊逻辑
宠物疫苗预约有以下几个特殊需求:
- 需要记录疫苗批号和生产厂商
- 同一疫苗需要多次接种(如狂犬疫苗需要3次)
- 接种后需要观察30分钟才能离开
我们在数据库中设计了专门的疫苗预约表:
sql复制CREATE TABLE `vaccine_appointment` (
`id` bigint NOT NULL AUTO_INCREMENT,
`appointment_id` bigint NOT NULL COMMENT '关联主预约ID',
`vaccine_type` varchar(50) NOT NULL COMMENT '疫苗类型',
`batch_no` varchar(100) NOT NULL COMMENT '疫苗批号',
`manufacturer` varchar(100) NOT NULL COMMENT '生产厂商',
`dose_number` tinyint NOT NULL COMMENT '第几针',
`next_appointment_date` date DEFAULT NULL COMMENT '下次接种日期',
`observation_notes` text COMMENT '观察记录',
PRIMARY KEY (`id`),
KEY `idx_appointment_id` (`appointment_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
4.2 多宠物同时就诊处理
很多用户会同时携带多只宠物就诊,我们设计了"主预约+子预约"的模式:
- 创建主预约记录,生成总订单
- 为每只宠物创建子预约记录
- 前台可以批量办理签到
- 医生端可查看关联的所有宠物信息
后端接口示例:
java复制@PostMapping("/batch")
public Result createBatchAppointment(@Valid @RequestBody BatchAppointmentDTO dto) {
// 1. 创建主订单
MasterAppointment master = new MasterAppointment();
master.setUserId(dto.getUserId());
master.setTotalAmount(calculateTotalFee(dto.getPets()));
masterAppointmentMapper.insert(master);
// 2. 创建子订单
List<SubAppointment> subs = dto.getPets().stream()
.map(pet -> {
SubAppointment sub = new SubAppointment();
sub.setMasterId(master.getId());
sub.setPetId(pet.getId());
sub.setScheduleId(dto.getScheduleId());
return sub;
})
.collect(Collectors.toList());
subAppointmentMapper.batchInsert(subs);
// 3. 返回结果
return Result.success(master.getId());
}
5. 性能优化实践
5.1 预约高峰期的应对策略
在"双十一"等促销活动期间,预约量可能暴增10倍,我们采取了以下措施:
- Redis缓存预热:提前加载未来3天的医生排班信息
- 读写分离:查询走从库,写入走主库
- 限流措施:
- 接口级别:使用Guava RateLimiter
- 用户级别:每个账号5分钟内的最大预约次数
- 队列削峰:非核心操作放入RabbitMQ延迟队列
Redis缓存设计示例:
java复制public Schedule getScheduleWithCache(Long scheduleId) {
String key = "schedule:" + scheduleId;
String json = redisTemplate.opsForValue().get(key);
if (StringUtils.isNotBlank(json)) {
return JSON.parseObject(json, Schedule.class);
}
Schedule schedule = scheduleMapper.selectById(scheduleId);
if (schedule != null) {
redisTemplate.opsForValue().set(
key,
JSON.toJSONString(schedule),
30,
TimeUnit.MINUTES);
}
return schedule;
}
5.2 前端性能优化
针对低配设备用户的优化方案:
- 组件懒加载:
javascript复制const ClinicMap = defineAsyncComponent(() =>
import('./components/ClinicMap.vue')
)
- 虚拟滚动:处理超长医生列表
vue复制<template>
<RecycleScroller
class="scroller"
:items="doctors"
:item-size="72"
key-field="id"
>
<template #default="{ item }">
<DoctorCard :doctor="item" />
</template>
</RecycleScroller>
</template>
- Web Worker:将预约冲突检测等计算密集型任务放到worker线程
6. 安全防护措施
6.1 防黄牛脚本方案
宠物医院的专家号经常被黄牛抢占,我们实施了多重防护:
- 行为验证:预约前完成滑块验证
- 设备指纹:记录浏览器特征指纹
- 预约模式识别:
- 正常用户:浏览多个页面后预约
- 脚本行为:直接访问预约接口
- 信用评级系统:频繁取消的用户降低优先级
安全验证代码示例:
java复制public void checkSecurityPolicy(AppointmentDTO dto) {
// 1. 检查验证码
if (!captchaService.verify(dto.getCaptchaKey(), dto.getCaptchaCode())) {
throw new SecurityException("验证码错误");
}
// 2. 检查设备指纹
String deviceId = dto.getDeviceFingerprint();
if (securityDao.hasSuspiciousBehavior(deviceId)) {
throw new SecurityException("存在可疑行为");
}
// 3. 检查预约频率
int recentCount = appointmentDao.countRecentAppointments(
dto.getUserId(),
LocalDateTime.now().minusHours(1));
if (recentCount > 3) {
throw new SecurityException("操作过于频繁");
}
}
6.2 敏感数据保护
宠物医疗数据包含大量敏感信息,我们采取以下保护措施:
- 数据脱敏:病历展示时隐藏联系方式等
- 加密存储:使用AES加密诊断报告
- 权限隔离:
- 医生只能查看自己接诊的病例
- 前台无法查看完整病历内容
- 操作审计:所有数据查询记录日志
MyBatis数据脱敏示例:
java复制public class DataMaskingInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object result = invocation.proceed();
if (result instanceof List<?>) {
((List<?>) result).forEach(this::maskData);
} else {
maskData(result);
}
return result;
}
private void maskData(Object obj) {
if (obj instanceof PetInfo) {
PetInfo pet = (PetInfo) obj;
pet.setOwnerPhone(maskPhone(pet.getOwnerPhone()));
}
}
private String maskPhone(String phone) {
if (StringUtils.isBlank(phone)) return "";
return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}
}
7. 部署与监控方案
7.1 容器化部署
使用Docker Compose编排关键服务:
yaml复制version: '3.8'
services:
app:
image: petclinic-backend:${TAG:-latest}
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- REDIS_HOST=redis
depends_on:
- redis
- mysql
mysql:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=${DB_ROOT_PASS}
- MYSQL_DATABASE=petclinic
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:6-alpine
ports:
- "6379:6379"
adminer:
image: adminer
ports:
- "8081:8080"
volumes:
mysql_data:
7.2 监控指标设计
使用Prometheus监控关键业务指标:
-
预约相关指标:
- 预约成功率
- 平均预约耗时
- 预约取消率
-
系统健康指标:
- 接口响应时间P99
- 数据库连接池使用率
- JVM内存使用情况
-
业务预警指标:
- 同一IP高频访问
- 异常时段预约请求
- 失败登录尝试
SpringBoot监控配置示例:
java复制@Configuration
@EnablePrometheusEndpoint
public class MonitorConfig {
@Bean
public CollectorRegistry collectorRegistry() {
CollectorRegistry registry = new CollectorRegistry();
// 预约相关指标
Gauge.builder("appointment_total",
() -> getCurrentAppointmentCount())
.description("当前有效预约数")
.register(registry);
// 接口性能指标
Summary.builder("api_response_time")
.quantile(0.5, 0.05)
.quantile(0.9, 0.01)
.register(registry);
return registry;
}
}
在项目上线后,我们发现最大的挑战不是技术实现,而是如何平衡不同角色的需求。比如医生希望预约间隔更长以保证问诊质量,而用户则希望有更多可预约时段。最终我们引入了智能排班算法,根据历史数据动态调整每位医生的接诊间隔,既保证了医疗质量,又提高了资源利用率。
