SpringBoot3+Vue3+微信小程序构建医院预约挂号系统实战
医院预约挂号系统是医疗信息化中连接患者与医院服务的关键入口这类系统既要保证患者操作的便捷性又要处理复杂的业务规则和数据一致性。基于 SpringBoot3、Vue3 和微信小程序的组合可以构建一个前后端分离、移动端友好的现代化预约系统。下面将按照实际项目开发流程从技术选型、环境搭建到核心功能实现完整介绍如何构建这样一个系统。1. 技术栈选型与环境准备医院预约挂号系统需要同时支持微信小程序前端交互、Web管理后台和稳定的后端服务。技术栈的选择直接影响开发效率和系统性能。1.1 技术栈组成与版本要求后端采用 SpringBoot 3.x它内置了最新版本的 Spring Framework 6提供了更好的性能和新特性支持。前端管理后台使用 Vue 3.x 组合式 API微信小程序则使用原生开发框架。环境要求清单JDK 17 或更高版本SpringBoot 3.x 最低要求Node.js 16.x 或更高版本MySQL 8.0 或兼容版本Redis 7.x 用于缓存和会话管理Maven 3.6 或 Gradle 7.x微信开发者工具最新稳定版关键依赖版本对照表组件版本说明SpringBoot3.2.x使用 Jakarta EE 9 命名空间Vue3.3.x组合式 API 和script setup语法微信小程序基础库3.x支持云开发和新生命周期MyBatis-Plus3.5.x简化数据库操作Jedis4.4.xRedis Java 客户端1.2 开发环境配置步骤首先配置后端开发环境创建 SpringBoot 项目时选择 Web、MySQL、Redis 等基础依赖# 使用 Spring Initializr 创建项目 curl https://start.spring.io/starter.zip \ -d dependenciesweb,mysql,redis,mybatis \ -d typemaven-project \ -d languagejava \ -d bootVersion3.2.0 \ -d baseDirhospital-booking \ -o hospital-booking.zip解压后检查pom.xml中的关键依赖版本properties java.version17/java.version maven.compiler.source17/maven.compiler.source maven.compiler.target17/maven.compiler.target /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.2.0/version /dependency /dependencies前端环境使用 Vite 创建 Vue 3 项目比 Vue CLI 启动更快npm create vuelatest hospital-admin cd hospital-admin npm install微信小程序项目在开发者工具中创建选择不使用云服务手动配置项目结构。2. 数据库设计与核心表结构医院预约系统的数据模型需要涵盖患者信息、医生排班、科室管理、预约记录等核心业务实体。2.1 主要数据表设计科室表 (department)存储医院科室信息CREATE TABLE department ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 科室名称, description TEXT COMMENT 科室描述, status TINYINT DEFAULT 1 COMMENT 状态0-停用 1-启用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) COMMENT 医院科室表;医生表 (doctor)关联科室和排班信息CREATE TABLE doctor ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20) NOT NULL COMMENT 医生姓名, department_id BIGINT NOT NULL COMMENT 所属科室ID, title VARCHAR(20) COMMENT 职称, specialty TEXT COMMENT 擅长领域, avatar_url VARCHAR(255) COMMENT 头像URL, status TINYINT DEFAULT 1 COMMENT 状态0-停诊 1-接诊, FOREIGN KEY (department_id) REFERENCES department(id) ) COMMENT 医生信息表;排班表 (schedule)是系统的核心需要处理复杂的时间规则CREATE TABLE schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, doctor_id BIGINT NOT NULL, work_date DATE NOT NULL COMMENT 排班日期, time_slot TINYINT NOT NULL COMMENT 时间段1-上午 2-下午 3-晚上, total_count INT DEFAULT 30 COMMENT 总号源数, reserved_count INT DEFAULT 0 COMMENT 已预约数, status TINYINT DEFAULT 1 COMMENT 状态0-停诊 1-正常, FOREIGN KEY (doctor_id) REFERENCES doctor(id), UNIQUE KEY uk_doctor_time (doctor_id, work_date, time_slot) ) COMMENT 医生排班表;2.2 预约业务表设计预约记录表 (appointment)需要处理并发预约和状态流转CREATE TABLE appointment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, patient_id BIGINT NOT NULL COMMENT 患者ID, schedule_id BIGINT NOT NULL COMMENT 排班ID, order_no VARCHAR(32) UNIQUE NOT NULL COMMENT 预约单号, status TINYINT DEFAULT 0 COMMENT 状态0-待支付 1-已预约 2-已取消 3-已完成, symptom_desc TEXT COMMENT 症状描述, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, cancel_time DATETIME COMMENT 取消时间, FOREIGN KEY (schedule_id) REFERENCES schedule(id) ) COMMENT 预约记录表;实际项目中还需要患者表、用户认证表、支付记录表等。设计时要特别注意索引策略比如在schedule_id、patient_id、create_time上建立复合索引优化查询性能。3. 后端核心功能实现SpringBoot 后端需要提供 RESTful API 支持小程序和管理后台重点实现预约业务流程和并发控制。3.1 实体类与数据层配置使用 MyBatis-Plus 简化数据操作先配置实体类映射Data TableName(schedule) public class Schedule { TableId(type IdType.AUTO) private Long id; private Long doctorId; private LocalDate workDate; private Integer timeSlot; private Integer totalCount; private Integer reservedCount; private Integer status; TableField(exist false) private Doctor doctor; }创建对应的 Mapper 接口继承 MyBatis-Plus 的 BaseMapperpublic interface ScheduleMapper extends BaseMapperSchedule { Select(SELECT * FROM schedule WHERE doctor_id #{doctorId} AND work_date BETWEEN #{startDate} AND #{endDate} ORDER BY work_date, time_slot) ListSchedule selectByDoctorAndDateRange(Param(doctorId) Long doctorId, Param(startDate) LocalDate startDate, Param(endDate) LocalDate endDate); }3.2 预约业务服务实现预约服务需要处理号源库存的并发控制使用 Redis 分布式锁防止超订Service public class AppointmentService { Autowired private RedisTemplateString, String redisTemplate; Transactional public AppointmentResult makeAppointment(AppointmentRequest request) { String lockKey schedule_lock: request.getScheduleId(); String lockValue UUID.randomUUID().toString(); try { // 获取分布式锁避免并发超订 Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, lockValue, Duration.ofSeconds(10)); if (!Boolean.TRUE.equals(locked)) { return AppointmentResult.error(系统繁忙请稍后重试); } Schedule schedule scheduleMapper.selectById(request.getScheduleId()); if (schedule null || schedule.getStatus() 0) { return AppointmentResult.error(号源不存在或已停诊); } if (schedule.getReservedCount() schedule.getTotalCount()) { return AppointmentResult.error(号源已满); } // 更新号源数量 int updateCount scheduleMapper.updateReservedCount( schedule.getId(), schedule.getReservedCount() 1); if (updateCount 0) { return AppointmentResult.error(号源库存变化请重新选择); } // 创建预约记录 Appointment appointment new Appointment(); appointment.setPatientId(request.getPatientId()); appointment.setScheduleId(request.getScheduleId()); appointment.setOrderNo(generateOrderNo()); appointment.setSymptomDesc(request.getSymptomDesc()); appointmentMapper.insert(appointment); return AppointmentResult.success(预约成功, appointment.getOrderNo()); } finally { // 释放锁 if (lockValue.equals(redisTemplate.opsForValue().get(lockKey))) { redisTemplate.delete(lockKey); } } } }3.3 控制器层与 API 设计RESTful API 设计要符合业务语义使用统一的响应格式RestController RequestMapping(/api/appointment) public class AppointmentController { Autowired private AppointmentService appointmentService; PostMapping(/create) public ApiResultAppointmentResult createAppointment( Valid RequestBody AppointmentRequest request) { try { AppointmentResult result appointmentService.makeAppointment(request); return ApiResult.success(result); } catch (Exception e) { log.error(预约失败, e); return ApiResult.error(系统异常预约失败); } } GetMapping(/list) public ApiResultListAppointmentVO getAppointmentList( RequestParam Long patientId, RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size) { PageAppointment appointmentPage appointmentService .getPatientAppointments(patientId, page, size); ListAppointmentVO voList convertToVOList(appointmentPage.getRecords()); return ApiResult.success(voList); } }统一的响应包装类确保前端处理逻辑一致Data public class ApiResultT { private Integer code; private String message; private T data; private Long timestamp; public static T ApiResultT success(T data) { ApiResultT result new ApiResult(); result.setCode(200); result.setMessage(success); result.setData(data); result.setTimestamp(System.currentTimeMillis()); return result; } }4. Vue3 管理后台开发管理后台需要提供科室管理、医生排班、预约统计等功能使用 Vue 3 组合式 API 提高代码可维护性。4.1 项目结构与组件设计采用基于路由的代码分割按功能模块组织代码src/ ├── components/ # 可复用组件 │ ├── ScheduleTable.vue │ └── DoctorSelector.vue ├── views/ # 页面组件 │ ├── Department.vue │ ├── Schedule.vue │ └── Appointment.vue ├── stores/ # Pinia 状态管理 │ ├── user.js │ └── schedule.js └── api/ # API 接口 ├── department.js └── appointment.js4.2 排班管理功能实现排班管理页面需要处理日期选择、医生筛选和批量操作template div classschedule-management el-form :modelqueryForm inline el-form-item label科室 el-select v-modelqueryForm.departmentId clearable el-option v-fordept in departmentList :keydept.id :labeldept.name :valuedept.id / /el-select /el-form-item el-form-item label日期范围 el-date-picker v-modelqueryForm.dateRange typedaterange value-formatYYYY-MM-DD range-separator至 start-placeholder开始日期 end-placeholder结束日期 / /el-form-item el-form-item el-button typeprimary clickloadSchedules查询/el-button /el-form-item /el-form ScheduleTable :datascheduleList updatehandleScheduleUpdate / /div /template script setup import { ref, onMounted } from vue import { getSchedules, updateSchedule } from /api/schedule import { getDepartments } from /api/department const queryForm ref({ departmentId: null, dateRange: [] }) const scheduleList ref([]) const departmentList ref([]) const loadSchedules async () { const params { departmentId: queryForm.value.departmentId, startDate: queryForm.value.dateRange?.[0], endDate: queryForm.value.dateRange?.[1] } try { const { data } await getSchedules(params) scheduleList.value data } catch (error) { console.error(加载排班数据失败, error) } } const handleScheduleUpdate async (schedule) { try { await updateSchedule(schedule.id, schedule) ElMessage.success(更新成功) loadSchedules() } catch (error) { ElMessage.error(更新失败) } } onMounted(async () { const { data } await getDepartments() departmentList.value data // 默认加载最近7天的排班 const endDate new Date() const startDate new Date(endDate.getTime() - 7 * 24 * 60 * 60 * 1000) queryForm.value.dateRange [ startDate.toISOString().split(T)[0], endDate.toISOString().split(T)[0] ] loadSchedules() }) /script4.3 API 接口封装与错误处理使用 Axios 封装统一的请求拦截器和错误处理// api/request.js import axios from axios import { ElMessage } from element-plus const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }) service.interceptors.request.use( config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer ${token} } return config }, error { return Promise.reject(error) } ) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message || Error)) } return res }, error { ElMessage.error(网络错误或服务器异常) return Promise.reject(error) } ) export default service5. 微信小程序端开发微信小程序作为患者入口需要优化用户体验确保预约流程顺畅。5.1 小程序页面结构与导航设计四个主要页面首页、科室列表、医生详情、我的预约。{ pages: [ pages/index/index, pages/department/list, pages/doctor/detail, pages/appointment/list, pages/appointment/detail ], tabBar: { list: [ { pagePath: pages/index/index, text: 首页, iconPath: images/home.png, selectedIconPath: images/home-active.png }, { pagePath: pages/appointment/list, text: 我的预约, iconPath: images/appointment.png, selectedIconPath: images/appointment-active.png } ] } }5.2 首页与科室选择功能首页展示医院信息和快捷入口科室列表支持按字母排序和搜索// pages/department/list.js Page({ data: { departments: [], searchValue: , alphabet: [A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z] }, onLoad() { this.loadDepartments() }, async loadDepartments() { try { const res await wx.request({ url: https://your-api.com/api/departments, method: GET }) if (res.data.code 200) { this.setData({ departments: this.groupByAlphabet(res.data.data) }) } } catch (error) { wx.showToast({ title: 加载失败, icon: none }) } }, groupByAlphabet(list) { const grouped {} this.data.alphabet.forEach(letter { grouped[letter] list.filter(dept dept.pinyin.charAt(0).toUpperCase() letter ) }) return grouped }, onSearch(e) { this.setData({ searchValue: e.detail.value }) } })对应的 WXML 模板实现字母导航和科室列表view classdepartment-page view classsearch-bar van-search value{{searchValue}} placeholder搜索科室 bind:changeonSearch / /view scroll-view classalphabet-nav scroll-y view wx:for{{alphabet}} wx:key*this classalphabet-item {{currentLetter item ? active : }} bindtaponLetterTap >// pages/doctor/detail.js Page({ data: { doctor: {}, schedules: [], selectedDate: , selectedSlot: null }, onLoad(options) { this.doctorId options.id this.loadDoctorDetail() this.loadSchedules() }, async loadDoctorDetail() { const res await wx.request({ url: https://your-api.com/api/doctors/${this.doctorId} }) this.setData({ doctor: res.data.data }) }, async loadSchedules() { const startDate this.getCurrentDate() const endDate this.getDateAfter(7) // 加载未来7天排班 const res await wx.request({ url: https://your-api.com/api/schedules, data: { doctorId: this.doctorId, startDate, endDate } }) this.setData({ schedules: this.groupSchedulesByDate(res.data.data), selectedDate: startDate }) }, groupSchedulesByDate(schedules) { const grouped {} schedules.forEach(schedule { const date schedule.workDate if (!grouped[date]) { grouped[date] [] } grouped[date].push(schedule) }) return grouped }, onDateSelect(e) { this.setData({ selectedDate: e.currentTarget.dataset.date, selectedSlot: null }) }, onSlotSelect(e) { const slot e.currentTarget.dataset.slot if (slot.reservedCount slot.totalCount) { wx.showToast({ title: 号源已满, icon: none }) return } this.setData({ selectedSlot: slot }) }, makeAppointment() { if (!this.data.selectedSlot) { wx.showToast({ title: 请选择时间段, icon: none }) return } wx.navigateTo({ url: /pages/appointment/confirm?scheduleId${this.data.selectedSlot.id} }) } })6. 系统集成与部署配置三个子系统需要协同工作配置统一的认证、数据格式和错误处理机制。6.1 跨域配置与安全策略SpringBoot 后端配置 CORS 支持小程序和管理后台访问Configuration public class WebConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }API 安全配置使用 JWT 进行身份验证Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeHttpRequests(authz - authz .requestMatchers(/api/auth/**).permitAll() .requestMatchers(/api/public/**).permitAll() .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } }6.2 生产环境部署配置使用 Docker 容器化部署编写 Dockerfile 和 docker-compose.yml# 后端 Dockerfile FROM openjdk:17-jdk-slim VOLUME /tmp COPY target/hospital-booking-0.0.1-SNAPSHOT.jar app.jar ENTRYPOINT [java,-jar,/app.jar]# docker-compose.yml version: 3.8 services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: your_password MYSQL_DATABASE: hospital_booking volumes: - mysql_data:/var/lib/mysql redis: image: redis:7-alpine command: redis-server --appendonly yes volumes: - redis_data:/data backend: build: ./backend ports: - 8080:8080 environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/hospital_booking SPRING_REDIS_HOST: redis depends_on: - mysql - redis6.3 微信小程序配置要点小程序开发设置中配置服务器域名request 合法域名https://your-api.comuploadFile 合法域名https://your-api.comdownloadFile 合法域名https://your-api.com在app.js中统一处理登录和会话管理App({ onLaunch() { this.checkLogin() }, async checkLogin() { const token wx.getStorageSync(token) if (token) { // 验证 token 是否有效 const valid await this.validateToken(token) if (!valid) { this.login() } } else { this.login() } }, login() { wx.login({ success: async (res) { if (res.code) { const loginRes await wx.request({ url: https://your-api.com/api/auth/wxlogin, method: POST, data: { code: res.code } }) if (loginRes.data.code 200) { wx.setStorageSync(token, loginRes.data.data.token) wx.setStorageSync(userInfo, loginRes.data.data.userInfo) } } } }) } })7. 常见问题排查与优化建议实际部署和运行过程中会遇到各种问题提前了解排查路径能节省大量调试时间。7.1 预约业务并发问题处理问题现象热门医生的号源出现超订同一时间段被多个患者预约。排查步骤检查数据库隔离级别推荐使用 REPEATABLE_READ验证 Redis 分布式锁的有效期和释放机制检查更新号源数量的 SQL 语句是否使用原子操作优化方案// 使用乐观锁防止超订 Update(UPDATE schedule SET reserved_count reserved_count 1, version version 1 WHERE id #{id} AND reserved_count total_count AND version #{version}) int updateWithOptimisticLock(Param(id) Long id, Param(version) Integer version);7.2 微信登录失败排查问题现象小程序端调用 wx.login 成功但后端验证 code 失败。排查顺序检查小程序 AppID 和 AppSecret 配置是否正确验证网络请求是否被拦截查看微信开发者工具 Network 面板检查后端调用微信 API 的 URL 和参数格式查看微信接口返回的错误码和信息代码层预防Service public class WeChatAuthService { public WxSessionInfo code2Session(String code) { String url String.format( https://api.weixin.qq.com/sns/jscode2session?appid%ssecret%sjs_code%sgrant_typeauthorization_code, appId, appSecret, code); try { ResponseEntityString response restTemplate.getForEntity(url, String.class); WxSessionInfo sessionInfo objectMapper.readValue(response.getBody(), WxSessionInfo.class); if (sessionInfo.getErrcode() ! null sessionInfo.getErrcode() ! 0) { log.error(微信登录失败: {}, sessionInfo.getErrmsg()); throw new RuntimeException(微信认证失败); } return sessionInfo; } catch (Exception e) { log.error(调用微信接口异常, e); throw new RuntimeException(网络异常请重试); } } }7.3 性能优化建议数据库优化为频繁查询的字段建立索引如schedule(work_date, doctor_id)分页查询使用limit避免全表扫描定期清理过期预约记录归档历史数据缓存策略科室列表、医生信息等基础数据使用 Redis 缓存排班信息设置合理过期时间平衡实时性和性能使用缓存预热机制提前加载热点数据前端优化小程序使用分包加载减少首次启动时间Vue 管理后台使用路由懒加载和组件异步加载图片资源使用 CDN 加速和适当压缩医院预约挂号系统的开发涉及多个技术栈的集成重点在于保证业务逻辑的正确性和数据一致性。实际项目中还需要考虑医疗行业的特殊要求如数据隐私保护、系统稳定性保障和审计日志等。这个技术栈组合为现代 Web 应用开发提供了完整的解决方案可以在此基础上根据具体需求进行功能扩展和优化。