Spring Boot+Vue+UniApp医院挂号小程序:从零构建毕设级全栈项目
如果你正在为计算机专业的毕业设计、课程设计或者个人项目寻找一个“能跑通、有深度、技术栈主流、文档齐全”的完整项目那么这篇文章就是为你准备的。很多同学在选题时都会陷入一个误区要么选一个过于简单的增删改查系统显得技术含量不足要么选一个过于复杂的商业项目导致无法按期完成代码逻辑混乱。今天要拆解的这个“医院挂号预约小程序”项目恰好踩在了这个平衡点上——它基于 Spring Boot Vue UniApp 这一当前企业级开发中最主流的技术栈业务场景贴近生活功能模块清晰非常适合作为学习、练手乃至毕业设计的蓝本。更重要的是这个项目不仅仅是一个“玩具”。它完整地覆盖了从后端 API 设计、数据库建模到前端 Vue 页面交互再到跨端 UniApp 小程序发布的整个开发生命周期。通过剖析它你不仅能学会如何将 Spring Boot、Vue、UniApp 这三个技术点串联起来更能理解一个真实可用的系统是如何从零开始构建的。本文将带你深入这个项目的核心从环境搭建、代码解读到部署上线并提供一套完整的实践指南和避坑清单确保你能真正将其复现并转化为自己的知识。1. 这个项目解决了什么实际问题在开始技术细节之前我们首先要明确为什么要做“医院挂号预约”这个场景它背后的技术挑战和价值是什么传统的医院挂号存在几个明显的痛点患者需要早起排队、现场信息不透明不知道哪个医生有号、流程繁琐。一个线上预约系统核心价值在于流程线上化和信息透明化。对于开发者而言这个业务场景能很好地锻炼以下几项能力用户角色与权限管理系统至少涉及患者、医生、管理员三种角色权限控制RBAC是必须实现的。复杂状态流转一个挂号订单会经历“待支付”、“已预约”、“已取消”、“已完成”等多种状态这涉及到状态机设计。时间与资源冲突处理这是核心业务逻辑。如何确保同一个医生在同一时间段不被重复预约如何处理退号后号源的释放这考验你的数据库事务和业务逻辑严谨性。前后端数据交互从科室、医生列表的查询到预约时间的选择再到订单的创建与支付模拟涉及大量的 API 设计与联调。多端适配使用 UniApp 开发微信小程序意味着你要处理微信生态的登录、授权、支付如需等特有 API以及小程序的页面生命周期。因此这个项目远不止是“增删改查”。它是一个微型的、但五脏俱全的“业务系统”能让你在实战中理解上述概念这正是它作为毕设或学习项目含金量高的原因。2. 技术栈选型与架构解析本项目采用了经典且流行的前后端分离架构。[微信小程序/UniApp前端] -- HTTP/WebSocket -- [Spring Boot后端] -- [MySQL数据库] ↑ (跨端编译)2.1 后端技术栈Spring Boot核心框架Spring Boot 2.x。它提供了自动配置、内嵌服务器等特性能让你快速搭建一个可独立运行的 RESTful API 服务。数据持久层MyBatis-Plus。这是对 MyBatis 的增强工具提供了通用的 CRUD 操作能极大减少单表操作的 SQL 编写工作量非常适合快速开发。数据库MySQL 5.7/8.0。关系型数据库用于存储用户、科室、医生、排班、订单等具有强关联性的数据。权限控制Spring Security 或 Apache Shiro。通常使用 JWT (JSON Web Token) 实现无状态的认证与授权这是前后端分离项目的标准做法。其他Lombok简化JavaBean、Swagger/knife4jAPI文档生成、Redis可选用于缓存科室医生信息或短信验证码。2.2 前端技术栈Vue UniApp核心框架Vue 3 (Composition API) 或 Vue 2。Vue 的响应式和组件化开发模式是构建复杂单页应用SPA的高效选择。跨端方案UniApp。这是一个使用 Vue.js 开发所有前端应用的框架编写一套代码可发布到 iOS、Android、Web 以及各种小程序微信、支付宝等。对于本项目主要输出目标是微信小程序。UI 框架uni-ui 或 uView UI。这是基于 UniApp 的 UI 组件库提供了按钮、列表、弹窗、表单等小程序常用组件能大幅提升开发效率。状态管理Vuex (Vue2) 或 Pinia (Vue3)。用于管理跨组件的共享状态如用户登录信息、全局配置等。网络请求uni.request 或封装后的 axios。用于与后端 Spring Boot API 进行通信。2.3 为什么是这套组合市场匹配度高Spring Boot 和 Vue 是当前国内 Java 和前端领域最主流的框架学习资源丰富企业应用广泛。开发效率高MyBatis-Plus 和 UniApp 的 UI 库都极大地减少了重复性编码工作。项目结构清晰前后端分离使得职责明确后端专注数据和业务逻辑前端专注交互和展示便于团队协作和后期维护。成果展示性好最终能产出一个可在微信中直接扫码体验的“小程序”对于毕设答辩或个人作品集而言展示效果远超一个只能在本地浏览器打开的管理后台。3. 环境准备与项目初始化在开始编码之前请确保你的开发环境已就绪。3.1 后端环境准备JDK安装 JDK 8 或 JDK 11推荐 JDK 8兼容性最好。在命令行输入java -version验证。Maven安装 Apache Maven 用于管理项目依赖和构建。输入mvn -v验证。IDE推荐使用 IntelliJ IDEA社区版或旗舰版它对 Spring Boot 支持极佳。MySQL安装 MySQL 5.7 或 8.0并启动服务。建议使用图形化工具如 Navicat 或 MySQL Workbench 进行管理。Redis可选如果项目用到缓存或会话管理需要安装并启动 Redis。3.2 前端环境准备Node.js安装 Node.js推荐 LTS 版本它自带了 npm 包管理器。输入node -v和npm -v验证。HBuilderX这是 DCloud 官方推出的 UniApp 开发工具内置了运行和编译环境对小程序开发非常友好。你也可以使用 VS Code 安装 UniApp 插件但 HBuilderX 的集成度更高。微信开发者工具用于调试和预览微信小程序。务必在微信公众平台注册一个小程序账号个人类型即可获取 AppID。3.3 初始化项目结构通常这类项目会提供完整的源码。你需要做的是后端将 Spring Boot 项目导入 IDEA。等待 Maven 自动下载依赖。前端在 HBuilderX 中导入 UniApp 项目或在项目目录下执行npm install安装依赖。数据库运行项目 SQL 目录下的init.sql脚本创建数据库和表结构并插入必要的初始数据如科室信息、管理员账号。关键一步配置文件修改后端项目的application.yml或application.properties文件必须根据你的本地环境修改。# application.yml 示例 server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/hospital_booking?useUnicodetruecharacterEncodingutf-8serverTimezoneAsia/Shanghai username: root # 改为你的数据库用户名 password: 123456 # 改为你的数据库密码 # Redis配置如果使用 redis: host: localhost port: 6379 password: database: 0 # JWT配置 jwt: secret: your-secret-key-here # 替换为一个足够复杂的密钥 expire: 604800 # token过期时间秒例如7天 header: Authorization前端项目通常有一个config.js或直接在main.js中配置后端 API 基础地址。// config.js 或 main.js 中 const baseUrl http://localhost:8080; // 后端服务地址如果是小程序真机调试需改为内网IP或部署后的域名 export default { baseUrl };4. 核心功能模块与代码实现拆解让我们深入项目内部看看几个核心功能是如何实现的。4.1 数据库设计核心表一个简化的核心表结构如下这有助于理解业务逻辑user用户表患者、医生、管理员通过role字段区分。department科室表。doctor医生表关联科室。schedule医生排班表包含医生ID、日期、时间段、总号源数、剩余号源数等。这是实现预约的关键表。order预约订单表关联用户、排班包含订单状态、创建时间等。4.2 后端核心接口实现Spring Boot MyBatis-Plus1. 医生排班查询接口这是患者选择医生和时间的入口。// ScheduleController.java RestController RequestMapping(/api/schedule) Api(tags 医生排班管理) public class ScheduleController { Autowired private ScheduleService scheduleService; GetMapping(/list) ApiOperation(根据科室和日期查询排班) public ResultListScheduleVO listSchedule( RequestParam(required false) Long deptId, RequestParam DateTimeFormat(pattern yyyy-MM-dd) Date date) { ListScheduleVO list scheduleService.listByDeptAndDate(deptId, date); return Result.success(list); } }// ScheduleServiceImpl.java Service public class ScheduleServiceImpl extends ServiceImplScheduleMapper, Schedule implements ScheduleService { Override public ListScheduleVO listByDeptAndDate(Long deptId, Date date) { QueryWrapperSchedule queryWrapper new QueryWrapper(); if (deptId ! null) { queryWrapper.eq(dept_id, deptId); } // 查询指定日期的排班且剩余号源大于0 queryWrapper.eq(work_date, date) .gt(remaining, 0) .orderByAsc(time_slot); ListSchedule schedules this.list(queryWrapper); // 将 Schedule 实体转换为 ScheduleVO视图对象并关联查询医生、科室信息 return schedules.stream().map(schedule - { ScheduleVO vo new ScheduleVO(); BeanUtils.copyProperties(schedule, vo); // 关联查询医生信息 Doctor doctor doctorService.getById(schedule.getDoctorId()); vo.setDoctorName(doctor.getName()); vo.setTitle(doctor.getTitle()); // ... 关联其他信息 return vo; }).collect(Collectors.toList()); } }2. 创建预约订单接口这是最核心的业务逻辑需要处理并发和事务。// OrderController.java RestController RequestMapping(/api/order) public class OrderController { Autowired private OrderService orderService; PostMapping(/create) ApiOperation(创建预约订单) public ResultString createOrder(RequestBody OrderCreateDTO dto, HttpServletRequest request) { // 从JWT token中获取当前用户ID Long userId JwtUtil.getUserIdFromToken(request); String orderId orderService.createOrder(userId, dto); return Result.success(预约成功, orderId); } }// OrderServiceImpl.java Service Transactional(rollbackFor Exception.class) // 开启事务 public class OrderServiceImpl extends ServiceImplOrderMapper, Order implements OrderService { Override public String createOrder(Long userId, OrderCreateDTO dto) { // 1. 校验排班信息是否存在且有余号 Schedule schedule scheduleService.getById(dto.getScheduleId()); if (schedule null || schedule.getRemaining() 0) { throw new BusinessException(号源已满或不存在); } // 2. 生成唯一订单号如HB20240520123456 String orderNo generateOrderNo(); // 3. 扣减号源使用乐观锁或悲观锁防止超卖 boolean updateSuccess scheduleService.lambdaUpdate() .eq(Schedule::getId, schedule.getId()) .eq(Schedule::getRemaining, schedule.getRemaining()) // 乐观锁条件 .set(Schedule::getRemaining, schedule.getRemaining() - 1) .update(); if (!updateSuccess) { throw new BusinessException(号源已被占用请重新选择); } // 4. 创建订单记录 Order order new Order(); order.setOrderNo(orderNo); order.setUserId(userId); order.setScheduleId(dto.getScheduleId()); order.setPatientName(dto.getPatientName()); order.setPatientPhone(dto.getPatientPhone()); order.setStatus(OrderStatusEnum.WAITING_PAY.getCode()); // 初始状态待支付 order.setAmount(schedule.getFee()); // 费用来自排班表 this.save(order); // 5. (模拟) 这里可以调用支付接口成功后更新订单状态为“已预约” // simulatePayment(orderNo); return orderNo; } private String generateOrderNo() { // 简单示例HB 年月日时分秒 随机数 SimpleDateFormat sdf new SimpleDateFormat(yyyyMMddHHmmss); String timeStr sdf.format(new Date()); int random (int) ((Math.random() * 9 1) * 1000); return HB timeStr random; } }关键点createOrder方法使用了Transactional注解。这意味着如果扣减号源成功但保存订单失败或者后续任何一步抛出异常整个操作都会回滚保证了数据的一致性。乐观锁 (eq(Schedule::getRemaining, schedule.getRemaining())) 是防止超卖的一种简单有效方式。4.3 前端核心页面实现UniApp Vue1. 首页科室与医生列表!-- pages/index/index.vue -- template view classcontainer !-- 顶部搜索/轮播图 -- swiper indicator-dots autoplay swiper-item v-for(item, index) in banners :keyindex image :srcitem.image modewidthFix/image /swiper-item /swiper !-- 科室导航 -- scroll-view scroll-x classdept-scroll view v-fordept in departmentList :keydept.id classdept-item clickselectDept(dept.id) image :srcdept.icon modeaspectFit/image text{{ dept.name }}/text /view /scroll-view !-- 医生列表 -- view classdoctor-list view v-fordoctor in doctorList :keydoctor.id classdoctor-card clicktoDoctorDetail(doctor.id) image classavatar :srcdoctor.avatar modeaspectFill/image view classinfo text classname{{ doctor.name }}/text text classtitle{{ doctor.title }}/text text classdept{{ doctor.deptName }}/text text classskill擅长{{ doctor.skill }}/text /view view classaction button sizemini typeprimary click.stoptoBooking(doctor.id)预约/button /view /view /view /view /template script import { getDepartments, getDoctorsByDept } from /api/index.js; export default { data() { return { banners: [], departmentList: [], doctorList: [], selectedDeptId: null }; }, onLoad() { this.loadDepartments(); this.loadDoctors(); }, methods: { async loadDepartments() { const res await getDepartments(); if (res.code 200) { this.departmentList res.data; } }, async loadDoctors() { const params { deptId: this.selectedDeptId }; const res await getDoctorsByDept(params); if (res.code 200) { this.doctorList res.data; } }, selectDept(deptId) { this.selectedDeptId deptId; this.loadDoctors(); }, toDoctorDetail(id) { uni.navigateTo({ url: /pages/doctor/detail?id${id} }); }, toBooking(doctorId) { uni.navigateTo({ url: /pages/booking/select?doctorId${doctorId} }); } } }; /script2. 预约页面选择时间与提交订单这个页面会调用我们之前写的排班查询和创建订单接口。!-- pages/booking/select.vue -- template view view classdate-selector !-- 日期选择器切换日期时重新加载排班 -- picker modedate :valueselectedDate changeonDateChange view选择日期{{ selectedDate }}/view /picker /view view classtime-slots view v-forslot in scheduleList :keyslot.id :class[slot-item, slot.remaining 0 ? disabled : ] clickselectSlot(slot) text{{ slot.timeSlot }} ({{ slot.remaining }}/{{ slot.total }})/text text v-ifslot.remaining 0已约满/text /view /view button typeprimary :disabled!selectedSlot clickconfirmBooking确认预约/button /view /template script import { getScheduleList, createOrder } from /api/booking.js; export default { data() { return { doctorId: , selectedDate: this.formatDate(new Date()), scheduleList: [], selectedSlot: null }; }, onLoad(options) { this.doctorId options.doctorId; this.loadSchedule(); }, methods: { formatDate(date) { // 返回 yyyy-MM-dd 格式 const year date.getFullYear(); const month (date.getMonth() 1).toString().padStart(2, 0); const day date.getDate().toString().padStart(2, 0); return ${year}-${month}-${day}; }, onDateChange(e) { this.selectedDate e.detail.value; this.loadSchedule(); }, async loadSchedule() { const params { doctorId: this.doctorId, date: this.selectedDate }; const res await getScheduleList(params); if (res.code 200) { this.scheduleList res.data; } }, selectSlot(slot) { if (slot.remaining 0) { this.selectedSlot slot; } }, async confirmBooking() { if (!this.selectedSlot) return; // 收集患者信息实际项目中可能从用户资料获取或弹窗输入 const orderData { scheduleId: this.selectedSlot.id, patientName: 张三, // 应替换为真实数据 patientPhone: 13800138000 }; uni.showLoading({ title: 提交中... }); try { const res await createOrder(orderData); uni.hideLoading(); if (res.code 200) { uni.showModal({ title: 预约成功, content: 您的预约单号为${res.data}, showCancel: false, success: () { uni.navigateTo({ url: /pages/order/detail?orderNo${res.data} }); } }); } else { uni.showToast({ title: res.msg || 预约失败, icon: none }); } } catch (error) { uni.hideLoading(); uni.showToast({ title: 网络错误, icon: none }); } } } }; /script5. 项目运行与效果验证5.1 启动后端服务在 IDEA 中找到主启动类通常命名为HospitalBookingApplication右键运行。观察控制台日志看到类似Tomcat started on port(s): 8080和Started ... in X seconds的日志表示启动成功。访问http://localhost:8080/doc.html如果集成了 knife4j或http://localhost:8080/swagger-ui.html查看并测试 API 接口。5.2 运行前端 UniApp 项目在 HBuilderX 中打开项目。点击顶部菜单运行-运行到小程序模拟器-微信开发者工具。HBuilderX 会自动编译项目并尝试启动微信开发者工具。首次运行需要你在微信开发者工具中导入项目路径通常是unpackage/dist/dev/mp-weixin。在微信开发者工具中点击“编译”和“预览”即可在模拟器或真机上看到小程序界面。5.3 核心流程验证请按顺序测试以下流程确保核心功能正常用户登录/注册能否成功获取 token首页加载科室和医生列表是否正常显示排班查询选择医生和日期后是否能正确显示可预约的时间段创建订单点击预约后是否能成功返回订单号同时对应时间段的剩余号源是否减少订单列表在“我的预约”页面能否看到刚创建的订单订单状态是否正确取消订单取消订单后号源是否被释放剩余号源数加16. 常见问题与排查思路在复现和开发过程中你几乎一定会遇到以下问题。这里提供排查思路。问题现象可能原因排查方式解决方案后端启动失败端口被占用8080 端口已被其他程序如另一个Spring Boot应用使用。1. 查看控制台错误信息。2. 命令行运行netstat -ano | findstr :8080(Windows) 或lsof -i:8080(Mac/Linux)。1. 终止占用端口的进程。2. 修改application.yml中的server.port为其他端口如8090。前端编译失败提示 npm 包错误1. 网络问题导致依赖未完整下载。2. node_modules 目录损坏。3. 包版本冲突。1. 查看 HBuilderX 控制台或命令行错误信息。2. 检查package.json中依赖版本。1. 删除node_modules目录和package-lock.json文件。2. 重新运行npm install或yarn。3. 使用npm cache clean --force清理缓存后重试。小程序真机预览白屏1. 后端 API 地址配置为localhost。2. 微信开发者工具未开启“不校验合法域名”。3. 服务器后端未启动或网络不通。1. 检查小程序开发者工具控制台 Network 请求是否失败。2. 检查请求 URL。1. 将后端 API 地址改为服务器的内网 IP如http://192.168.1.100:8080。2. 在微信开发者工具详情 - 本地设置中勾选“不校验合法域名...”。3.上线前必须配置合法域名。请求 API 返回 4041. 后端接口路径写错。2. 前端请求的 URL 拼接错误。3. 后端 Controller 未正确映射。1. 在浏览器或 Postman 中直接访问后端完整 URL 测试。2. 对比前端请求代码和后端RequestMapping注解路径。1. 确保后端接口能独立访问通。2. 检查前端config.js中的baseUrl和具体请求路径的拼接。请求 API 返回 401 (Unauthorized)1. 未登录或 token 过期。2. token 未在请求头中正确携带。3. 访问了需要特定角色权限的接口。1. 检查浏览器开发者工具或小程序调试器的请求头看Authorization字段是否存在且格式正确通常为Bearer token。2. 检查后端 token 校验逻辑。1. 确保登录流程成功并保存了 token。2. 在前端请求拦截器中统一添加 token。3. 检查接口权限注解如PreAuthorize(hasRole(USER))。数据库连接失败1. MySQL 服务未启动。2.application.yml中数据库配置URL、用户名、密码错误。3. 数据库驱动版本不匹配。1. 查看 Spring Boot 启动日志中的数据库连接错误。2. 使用数据库工具如 Navicat测试连接。1. 启动 MySQL 服务。2. 仔细核对配置文件注意数据库名、时区设置serverTimezoneAsia/Shanghai。3. 确认pom.xml中 MySQL 驱动版本。预约时出现“号源已被占用”高并发场景下乐观锁更新失败。查看订单创建逻辑特别是updateSuccess是否为 false。1. 这是正常业务逻辑提示用户重新选择。2. 对于高并发场景可以考虑更精细的锁机制或使用消息队列削峰。微信小程序登录失败1. 小程序 AppID 配置错误。2. 后端登录接口未正确调用微信服务端 API。3. 小程序未发布但使用了线上域名。1. 检查微信开发者工具和代码中的 AppID。2. 查看后端登录接口日志看调用微信 API 的返回值。1. 确保前后端 AppID 和 AppSecret 一致且正确。2. 在小程序管理后台配置服务器域名。3. 真机调试时在开发者工具开启“不校验域名”。7. 项目优化与最佳实践建议当你成功跑通基础功能后可以考虑从以下几个方面进行优化和深化这会让你的项目在答辩或面试中脱颖而出。7.1 后端优化接口安全SQL 注入坚持使用 MyBatis-Plus 的 QueryWrapper 或注解避免手动拼接 SQL。XSS 过滤对用户输入的富文本内容进行过滤或转义。参数校验使用Valid注解和 Hibernate Validator 对入参进行严格校验。频率限制对短信发送、登录等接口添加限流如使用 Redis Lua。性能提升缓存将不常变的科室、医生信息放入 Redis 缓存。数据库索引为schedule表的doctor_id,work_dateorder表的user_id,status等查询字段添加索引。分页查询列表接口如订单列表务必实现分页使用 MyBatis-Plus 的Page对象非常方便。代码结构DTO/VO 分离严格区分入参DTO、出参VO和数据库实体Entity避免实体对象直接暴露给前端。全局异常处理使用ControllerAdvice和ExceptionHandler统一处理异常返回友好的 JSON 格式错误信息。统一响应封装所有 API 返回统一的Result对象包含code,msg,data。7.2 前端优化用户体验加载状态所有网络请求都应配合加载动画如 uni.showLoading。下拉刷新与上拉加载列表页面实现此功能提升体验。图片懒加载使用 UniApp 的lazy-load属性。表单验证在提交前进行客户端验证减少无效请求。状态管理将用户信息、全局配置等存储在 Vuex/Pinia 中而不是每个页面都去localStorage读取。代码复用将通用的网络请求方法、工具函数如时间格式化抽取到独立的utils文件中。将可复用的 UI 组件如医生卡片、时间选择器抽取为公共组件。7.3 部署上线简化版后端使用mvn clean package打包生成jar文件。在服务器或本地测试上通过java -jar your-project.jar运行。可使用nohup或配置为系统服务。前端在 HBuilderX 中点击发行-小程序-微信生成生产代码包。在微信开发者工具中上传此代码包提交审核。数据库将本地数据库导出为 SQL 文件在服务器上导入。域名与 HTTPS小程序要求后端 API 必须使用 HTTPS 和备案域名。你需要购买域名、备案并配置 SSL 证书云服务商通常提供免费证书。8. 总结从项目到能力的跃迁通过这个“医院挂号预约小程序”项目你完成的不仅仅是一个可以运行的代码集合。你实践了一个完整的企业级应用开发流程需求分析、技术选型、数据库设计、后端 API 开发、前端页面实现、跨端编译、调试与部署。在这个过程中你接触了 Spring Boot 的自动配置与 starter 机制、MyBatis-Plus 的高效 CRUD、JWT 无状态认证、UniApp 的跨端开发范式以及微信小程序特有的生态。更重要的是你学会了如何解决真实问题如何处理业务状态流转如何保证数据一致性如号源不超卖如何设计友好的用户界面如何排查和解决跨端的网络问题这些经验远比单纯学习框架 API 更有价值。建议你在完全理解本项目的基础上尝试进行以下扩展这将使你的技能再上一个台阶集成短信服务实现真实的手机号注册和登录验证。模拟支付流程集成微信支付沙箱环境完成从下单到支付回调的完整流程。实现 Web 管理后台使用 Vue Element UI 或 React Ant Design 为管理员开发一个功能更全面的后台管理系统。容器化部署学习 Docker将后端、数据库、Redis 容器化并使用 Docker Compose 一键启动。这个项目是一个绝佳的起点和跳板。希望你能通过它不仅完成一个出色的毕业设计更能构建起自己独立开发全栈应用的能力体系。