简介:这是一套面向医院门诊场景的微信小程序预约挂号系统完整项目,包含前后端源码、数据库文件及配套论文,适合计算机专业学生用于毕业设计、课程设计或项目实战练习。压缩包共2000个文件,以PHP后端、Vue/JS前端逻辑、WXML/WXSS小程序页面为主,附带SQL数据库、Excel数据表以及一键部署脚本,整体大小约19.82MB,目录结构清晰便于按模块查阅。系统实现用户注册登录、科室医生信息查询、在线预约、支付取号、取消预约及后台排班管理等完整业务流程,覆盖小程序开发、API接口调用、微信支付集成、数据库设计等多项关键技术点。资源还包含后台管理端页面和论文文档,可帮助理解系统架构与实现思路。已有372人学习下载,对于需要快速上手项目开发的计算机专业学生,这是一份可直接运行、二次开发和写入简历的宝贵资料。
1. 微信小程序医院预约挂号系统:不只是一张小程序页面
微信小程序医院预约挂号系统,是毕业设计里出现频率很高的一个选题:前端一个小程序,后端一套接口,数据库几张表,再加一篇论文。它的价值不在技术难度,而在于把真实业务里的“预约”“排班”“退号”“爽约”这几个状态流转完整做了一遍。这个题目适合正在找毕业设计方向的计算机专业学生,也适合想用完整项目入门微信小程序开发的从业者。很多人以为难点在微信小程序的页面写法,实际情况是:页面一两天就能写完,号源排班和并发下的余号扣减,才是让项目翻车的地方。这篇内容就按“先建表、再写接口、最后填页面”的顺序,把每一步的可复现细节和坑位讲清楚。
2. 先把预约挂号拆成数据模型:数据库设计决定系统上限
2.1 科室、医生、用户:三张基础表的字段与索引
我一般拿到这种选题,先不急着建小程序页面,而是把数据库表画出来。预约挂号系统的实体关系不复杂,但表之间怎么关联、哪些字段参与业务状态流转,直接决定后面接口好不好写。热点词里那些“数据库”“增删改查”是真的核心,先拆表再写业务,可以少走很多弯路。
用户表存储小程序端授权登录的用户。核心字段:id 主键、openid 微信用户唯一标识、nickname 昵称、phone 手机号、create_time 注册时间。openid 需要加唯一索引,微信登录时要用它反查用户;如果允许手机号登录,phone 也建议加唯一索引。这里有个细节:不要把微信返回的 session_key 存进数据库。session_key 每次登录都会变,存它只会让“登录态过期”的排查更难。
科室表字段简单:id、name 科室名、intro 科室介绍。医院科室层级一般是“内科到心血管内科”这种两级结构,毕业设计做成一级就够;真要做两级,加一个 parent_id 字段,预留好总比后面改表舒服。
医生表要小心。字段包括:id、dept_id 所属科室、name 医生姓名、title 职称、intro 简介、week_schedule 出诊星期。week_schedule 很容易设计错,常见做法是存字符串“1,2,3,4,5”表示周一到周五出诊,也有用 JSON 数组存每天出诊时间段的。我更推荐后者,因为上午、下午的出诊时段不一样,单纯一个字符串表达不了。dept_id 加普通索引,因为科室页面要按它查医生列表。title 不要做成数字枚举,直接存“主任医师”“副主任医师”这种字符串,页面渲染少一次字典转换,论文里写起来也直观。
2.2 号源表与预约订单表:状态机才是灵魂
很多选这个题的人会把预约订单当成核心表,其实真正的核心是号源表 schedule。它描述“某医生在某个日期某个时间段放了多少个号”,预约订单只是对这个号的占用记录。
schedule 表字段:id、doctor_id 医生、dept_id 科室(冗余,方便查询)、work_date 出诊日期、start_time 开始时间、end_time 结束时间、total_count 总号数、booked_count 已约号数、status 状态、version 版本号。status 建议用 0 正常、1 已约满、2 停诊。已约满这个状态其实可以由 booked_count >= total_count 推导出来,但保留它能让列表页筛选更直接,接口少一次计算。
appointment 预约订单表字段:id、order_no 订单号、user_id 用户、doctor_id 医生、schedule_id 号源、appoint_date 预约日期、status 状态、create_time 下单时间、cancel_time 取消时间。
订单状态机是这套系统要讲清楚的重点。习惯定义成:0 待支付、1 已预约、2 已完成、3 已取消、4 爽约。待支付和已预约必须分开,很多医院要求先付费再占号;不想要支付环节的课设,可以直接从 0 变 1,但论文里的状态图就少了一个分支。爽约状态靠定时任务或用户主动操作触发:用户约了当天号源没到诊,也没取消,系统就把订单标记为 4。这个逻辑在答辩时很加分,因为它体现的不只是增删改查,而是把业务闭环想清楚了。
2.3 MySQL 建库建表脚本:用 Navicat 跑通初始化
数据库我用 MySQL 8.0,管理工具用 Navicat。下面是完整建表脚本,直接用 Navicat 新建查询执行,注意字符集、索引和外键这三个点。
-- 医院预约挂号系统核心表初始化脚本 CREATE DATABASE IF NOT EXISTS hospital_appointment DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE hospital_appointment; -- 用户表 CREATE TABLE `user` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `openid` VARCHAR(64) NOT NULL COMMENT '微信openid', `nickname` VARCHAR(50) DEFAULT '' COMMENT '昵称', `phone` VARCHAR(20) DEFAULT '' COMMENT '手机号', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 科室表 CREATE TABLE `department` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL COMMENT '科室名称', `intro` VARCHAR(500) DEFAULT '' COMMENT '科室介绍', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科室表'; -- 医生表 CREATE TABLE `doctor` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `dept_id` INT UNSIGNED NOT NULL COMMENT '所属科室', `name` VARCHAR(50) NOT NULL, `title` VARCHAR(30) DEFAULT '' COMMENT '职称', `intro` VARCHAR(500) DEFAULT '', `week_schedule` VARCHAR(100) NOT NULL DEFAULT '[]' COMMENT '出诊安排JSON', PRIMARY KEY (`id`), KEY `idx_dept` (`dept_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='医生表'; -- 号源表 CREATE TABLE `schedule` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `doctor_id` INT UNSIGNED NOT NULL, `dept_id` INT UNSIGNED NOT NULL, `work_date` DATE NOT NULL COMMENT '出诊日期', `start_time` TIME NOT NULL, `end_time` TIME NOT NULL, `total_count` INT NOT NULL DEFAULT 30 COMMENT '总号数', `booked_count` INT NOT NULL DEFAULT 0 COMMENT '已约号数', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0正常 1约满 2停诊', `version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (`id`), KEY `idx_doctor_date` (`doctor_id`, `work_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='号源表'; -- 预约订单表 CREATE TABLE `appointment` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL COMMENT '订单号', `user_id` INT UNSIGNED NOT NULL, `doctor_id` INT UNSIGNED NOT NULL, `schedule_id` INT UNSIGNED NOT NULL, `appoint_date` DATE NOT NULL COMMENT '预约就诊日期', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已预约 2已完成 3已取消 4爽约', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `cancel_time` DATETIME DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user_status` (`user_id`, `status`), KEY `idx_schedule` (`schedule_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预约订单表'; -- 黑名单表 CREATE TABLE `blacklist` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `user_id` INT UNSIGNED NOT NULL, `reason` VARCHAR(200) DEFAULT '' COMMENT '拉黑原因', `expire_time` DATETIME NOT NULL COMMENT '解禁时间', PRIMARY KEY (`id`), KEY `idx_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='黑名单表';这套脚本建库时先把数据库字符集固定成 utf8mb4,后面所有表默认继承。user、order在 MySQL 里是保留字,表名全用反引号包住,order_no 字段避开保留字。唯一索引uk_openid保证同一个微信用户只占一行,idx_doctor_date支撑“按医生查某天号源”的高频查询。三个重点解释一下。
第一,字符集一定要用 utf8mb4,不要用 utf8。小程序端昵称里一旦出现 emoji,utf8 表会直接报Incorrect string value。第二,外键我故意没建。毕业设计图方便删数据,用逻辑关联代替物理外键;真要加外键,Navicat 按脚本顺序导入时容易因为表引用顺序报错。第三,order_no 不要用自增 id 当订单号,应用层生成,格式可以是当天日期加时间戳加四位随机数,保证全局唯一且能看出下单时间。
提示:MySQL 8.0 默认认证插件是 caching_sha2_password,老版本 Navicat 连接会报 “Client does not support authentication protocol”。解决方法是把用户认证改回 mysql_native_password:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';,或者直接升级 Navicat。
3. 微信小程序端最小可运行代码:从登录授权到提交预约
3.1 项目结构与技术选型:原生微信小程序够用
很多人在“原生微信小程序还是 uniapp”之间纠结。uniapp 在热词里常被拿来对比 Android、iOS、鸿蒙的多端复用,好处是一套代码编译多端,代价是每端行为差异都要填坑。预约挂号这个题目的核心是业务本身,我的建议是用原生微信小程序,理由有三:官方文档就是现成的学习资料;预览调试路径最短;答辩被问到页面生命周期时能直接指到 Page 代码,不用隔着一层框架解释。
项目结构按功能拆目录,页面只放四个:首页科室列表、科室详情医生列表、预约提交页、我的预约列表。
├── app.js # 全局逻辑、登录态初始化 ├── app.json # 页面路由与 window 配置 ├── utils/ │ └── request.js # wx.request 统一封装 ├── pages/ │ ├── index/ # 首页:科室列表 │ ├── department/ # 科室详情:医生列表 │ ├── booking/ # 预约提交页 │ └── order/ # 我的预约列表app.json 里要注意微信小程序顶部导航栏高度。默认导航栏在不同机型上高度不一样,iPhone X 以上带刘海,Android 普遍 48px 左右。做排班页头部固定时建议用"navigationStyle": "custom"后自己算状态栏高度,否则真机预览会看到导航栏和内容重叠。不想自定义时,直接保留默认导航栏,在 app.json 里配置"navigationBarTitleText",省掉适配问题。
3.2 全局配置与 wx.request 请求封装
登录流程小程序端只做一件事:调用 wx.login 拿 code,发给后端换 token。token 过期时间要自己管理,不要依赖微信 session_key,因为 session_key 有效期不稳定,会出现“小程序还开着,后端已经说登录过期”的体验断裂。
// app.js - 小程序入口 App({ globalData: { token: '', userInfo: null }, onLaunch() { this.login() }, login() { wx.login({ success: (res) => { if (res.code) { // 把 code 发给后端,后端通过 code2session 换 openid 并返回自定义 token wx.request({ url: 'http://localhost:8080/api/auth/login', method: 'POST', data: { code: res.code }, success: (res) => { if (res.data.code === 0) { const token = res.data.data.token this.globalData.token = token // 本地缓存 token 和过期时间 wx.setStorageSync('token', token) wx.setStorageSync('token_expire', Date.now() + 7200 * 1000) } } }) } } }) } })token_expire 是我建议加的:很多项目只存 token 不存过期时间,用户隔天打开时 token 早已失效,接口返回 401 才跳登录,白屏一整个页面。小程序端先判断本地过期时间,失效直接走登录流程,这个细节写进论文里能体现你考虑过状态管理。http://localhost:8080是本地开发地址,真机预览时 localhost 指向手机本身,必须改成局域网 IP 或 HTTPS 域名,这是新手最容易“开发工具能跑、手机一打开全失败”的根因。
请求封装要解决三件事:统一注入 token、统一处理业务码、统一处理 401。下面的封装是我最常用的结构。
// utils/request.js - 微信小程序网络请求封装 const BASE_URL = 'http://192.168.1.100:8080/api' function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { const token = wx.getStorageSync('token') wx.request({ url: BASE_URL + path, method: method, data: data, header: { 'Content-Type': 'application/json', 'Authorization': token ? 'Bearer ' + token : '' }, success: (res) => { if (res.data.code === 0) { resolve(res.data.data) } else if (res.data.code === 401) { // token 过期,清缓存并跳登录 wx.removeStorageSync('token') wx.removeStorageSync('token_expire') wx.navigateTo({ url: '/pages/login/login' }) reject(res.data) } else { reject(res.data) } }, fail: (err) => reject(err) }) }) } module.exports = { request, BASE_URL }BASE_URL 独立到一个文件里,换后端地址时只改一处。后端返回格式统一成{ code, message, data },code 0 成功,401 表示登录态失效。这个约定要在前后端接口文档里写死,后面论文接口设计章节直接引用。
3.3 科室列表与医生排班页:WXML 渲染与预约提交
科室列表页用 3.2 的封装请求后端接口,加载完渲染到 WXML。医生排班页要根据 doctorId 查号源列表,每个号源显示日期、时段和余号,余号由totalCount - bookedCount计算,前端只展示不计算业务逻辑。
// pages/index/index.js - 科室列表 const { request } = require('../../utils/request') Page({ data: { departments: [], loading: false }, onLoad() { this.loadDepartments() }, async loadDepartments() { this.setData({ loading: true }) try { const departments = await request('/department/list', 'GET') this.setData({ departments: departments }) } catch (e) { wx.showToast({ title: '加载失败', icon: 'none' }) } finally { this.setData({ loading: false }) } } })预约提交是核心动作。用户点“确认预约”后前端把 doctorId 和 scheduleId 传给后端,按钮立刻禁用,等接口返回再恢复。这能拦住手滑连点,但真正防止重复订单要靠后端幂等控制,前端防重只是体验层面的第一道防线。
// pages/booking/booking.js - 预约提交 const { request } = require('../../utils/request') Page({ data: { doctorId: '', scheduleId: '', submitting: false }, onLoad(options) { this.setData({ doctorId: options.doctorId, scheduleId: options.scheduleId }) }, submitOrder() { if (this.data.submitting) return this.setData({ submitting: true }) request('/appointment/create', 'POST', { doctorId: this.data.doctorId, scheduleId: this.data.scheduleId }).then((data) => { wx.showToast({ title: '预约成功', icon: 'success' }) wx.redirectTo({ url: '/pages/order/order?id=' + data.appointmentId }) }).catch((err) => { wx.showToast({ title: err.message || '预约失败', icon: 'none' }) this.setData({ submitting: false }) }) } })对应 WXML 里按钮绑定 submitting 状态:
<view class="doctor-info"> <text class="name">{{doctorInfo.name}}</text> <text class="title">{{doctorInfo.title}}</text> </view> <view class="schedule-info"> <text>就诊日期:{{schedule.workDate}}</text> <text>号源剩余:{{schedule.totalCount - schedule.bookedCount}}</text> </view> <button class="submit-btn" disabled="{{submitting}}" bindtap="submitOrder"> {{submitting ? '提交中...' : '确认预约'}} </button>事件绑定里直接写方法名bindtap="submitOrder",不要写成bindtap="submitOrder()",后者在自定义组件里会失效,这是小程序开发里常见的玄学报错之一。数据绑定里做过一次减法计算,真实的项目建议把remainCount在 JS 里算好再塞进 data,WXML 只负责展示,排查问题时少一层逻辑。支付环节如果要做,就在 submitOrder 成功后调 wx.requestPayment 发起微信支付,后端返回支付参数;课设一般用模拟支付代替,把订单状态直接置为已支付,论文里注明生产环境需要对接微信支付商户平台。
到这里小程序端闭环了:授权登录拿 token、科室列表加载、医生排班展示、预约提交。整套代码量不大,但每一步都依赖后端接口约定,下面一章把后端接口和管理端逻辑补上。
4. 后端接口与管理端:排班、余号扣减与退号黑名单
4.1 接口选型:Spring Boot 还是 Node.js
后端技术栈在标题里没写死,但“源码”通常指后端完整工程。我见过最多的是 Spring Boot + MyBatis-Plus,也有用 Node.js Express 的。如果你已经会 Spring Boot,直接用,答辩时“事务管理”是个加分点;如果想整个项目全栈都用 JavaScript,Node.js 写起来快,但并发那块容易被追问底层实现。这里我按 Java + Spring Boot 风格写,因为 @Transactional 声明式事务和 MySQL 的配合最成熟,讲并发控制时更有底气。
接口清单建议固定一套约定,前后端联调按这个走:
POST /api/auth/login code 换 token GET /api/department/list 科室列表 GET /api/doctor/list 按科室查医生 GET /api/schedule/list 按医生查号源 POST /api/appointment/create 提交预约 POST /api/appointment/cancel 退号 GET /api/appointment/mine 我的预约
后端工程里我会单独抽一层 Service 处理业务,Controller 只做参数接收和结果包装。这样做的好处是事务边界清晰,排班、预约、退号这些核心方法都能用注解控制事务粒度。
4.2 预约接口事务与乐观锁:防止号源被抢超
号源超卖是预约系统最经典的并发问题。两个用户同时看到余号 1,同时提交,如果代码是先查 booked_count,判断小于 total_count,再更新,两个请求都能通过检查,最后超卖。解决这个要用数据库行锁或乐观锁,先看行锁版本:
/** * 预约接口 - 行锁版本 * 数据库事务里执行,锁住当前号源行,防止并发修改余号 */ @Override @Transactional(rollbackFor = Exception.class) public Long createAppointment(Long userId, Long scheduleId) { // 1. 查询号源并加行锁(SELECT ... FOR UPDATE) Schedule schedule = scheduleMapper.selectByIdForUpdate(scheduleId); if (schedule == null) { throw new BusinessException("号源不存在"); } if (schedule.getBookedCount() >= schedule.getTotalCount()) { throw new BusinessException("该时段号源已约满"); } // 2. 创建预约订单 Appointment appointment = new Appointment(); appointment.setOrderNo(generateOrderNo()); appointment.setUserId(userId); appointment.setScheduleId(scheduleId); appointment.setDoctorId(schedule.getDoctorId()); appointment.setAppointDate(schedule.getWorkDate()); appointment.setStatus(0); appointmentMapper.insert(appointment); // 3. 扣减余号 scheduleMapper.increaseBookedCount(scheduleId); return appointment.getId(); }selectByIdForUpdate 对应 SQL 是SELECT * FROM schedule WHERE id = ? FOR UPDATE,锁住这行直到事务提交。第二个请求进来在这行等锁,前一个事务提交后再读,读到的是更新后的 booked_count,超卖从根上堵住。注意 selectByIdForUpdate 这个查询必须在事务里执行,否则行锁会立刻释放,等于白写。
行锁的代价是高并发下排队,性能有上限。乐观锁是另一个方案,SQL 长这样:
UPDATE schedule SET booked_count = booked_count + 1, version = version + 1 WHERE id = #{scheduleId} AND booked_count < total_count AND version = #{oldVersion}Java 里先读一次 schedule 拿到旧 version,再执行乐观锁更新,影响行数为 0 说明有人抢先了,回滚订单并提示用户。乐观锁适合读多写少的场景,但医院号源是稀缺资源,写冲突不低,所以行锁更好讲、更直观。答辩时两个方案的区别是高频考点,至少能说清楚行锁锁行、乐观锁靠条件更新。
4.3 退号接口与黑名单管理:把爽约变成可运营数据
退号不能做成 DELETE 一条预约记录,正确做法是改状态。订单表关联号源、支付记录和后续就诊记录,物理删除会让统计无从谈起。退号接口做三件事:校验订单归属、改状态、释放号源。
@Override @Transactional(rollbackFor = Exception.class) public void cancelAppointment(Long userId, Long appointmentId) { Appointment appointment = appointmentMapper.selectById(appointmentId); // 1. 校验归属权和状态 if (appointment == null || !appointment.getUserId().equals(userId)) { throw new BusinessException("订单不存在"); } if (appointment.getStatus() == 2 || appointment.getStatus() == 4) { throw new BusinessException("已完成或爽约订单不能退号"); } // 2. 状态改为已取消 appointment.setStatus(3); appointment.setCancelTime(new Date()); appointmentMapper.updateById(appointment); // 3. 释放号源 scheduleMapper.decreaseBookedCount(appointment.getScheduleId()); }黑名单逻辑放在预约创建前:用户退号次数达到阈值,比如一周内退号超过 3 次,后端在创建预约前查 blacklist 表,命中就拦截。黑名单存 expire_time,到期自动失效,查询条件写expire_time > NOW()。这个模块很小,但能撑起论文“业务创新点”部分,比单纯增删改查有说服力得多。
排班展开逻辑也放在后端:管理员选一个医生和日期范围,系统遍历每一天,判断星期几是否匹配医生 week_schedule 里的配置,匹配则生成一条 schedule 记录,total_count 默认 30。周几判断用 Java 标准库LocalDate.getDayOfWeek().getValue(),返回 1 到 7 对应周一到周日;如果手写 getDay 或者用字符串截取,极容易踩到 0 优先还是 1 优先的坑,这条放在第 5 章当典型踩坑记录展开。
5. 避坑记录:医院预约挂号系统最容易翻车的 5 个环节
5.1 同一用户重复预约同一号源
现象:用户反复点“确认预约”,生成了多条预约记录,号源余量被扣了 3 次。
原因:前端做了按钮防重复,但绕过前端直接调接口,或者后端接口没做“是否已预约过”的校验。
解决:在 appointment 表的 user_id + schedule_id 加唯一索引,插入前查询当前用户是否已有未取消订单。状态为 3 的已取消记录允许重新预约,所以唯一索引约束不了“非取消状态”。我的做法是业务层先查有效订单再插入,数据库层再加 user_id + schedule_id + status 普通索引兜底,让重复数据在慢查询日志里更容易暴露。
5.2 并发下号源超卖
现象:压测 100 个并发请求预约最后 1 个号,成功 20 个,后台 booked_count 超过 total_count。
原因:接口先 SELECT 判断余号再 UPDATE 扣减,两次操作之间有间隙,并发请求都通过了判断。
解决:用 4.2 的SELECT ... FOR UPDATE行锁或乐观锁。行锁后 100 个并发请求最终只有 1 个成功,其余返回“号源约满”。这个坑值得写进论文测试章节:并发前后各查一次 booked_count,截图对比,答辩直接加分。
5.3 登录态过期与 code 重复使用
现象:小程序第一次打开正常,隔天再打开接口全部 401,或者连续操作时偶发登录失败。
原因:wx.login 的 code 只能用一次,有效期只有 5 分钟。有些项目把 code 缓存在本地反复传,后端用同一个 code 调 code2session,第二次直接报错。
解决:登录流程只在小程序启动或 token 过期时执行一次。后端登录接口用 code 换到 openid 后,生成自定义 token(UUID 或 JWT)并设置过期时间,小程序端像 3.2 那样存 token 和 token_expire。token 过期时间建议 2 小时,配 token_expire 在失效前主动跳登录,用户体验最顺。
5.4 排班展开时的星期与日期错位
现象:管理员设置“周一、周三出诊”,生成的号源里周二也在放号,或者周一反而没有号。
原因:week_schedule 存的是 0-6 还是 1-7 没约定。后端 JavagetDayOfWeek().getValue()返回 1-7,前端 JSnew Date().getDay()返回 0-6,两端直接拼字符串比较必然错位。
解决:统一约定 1-7 对应周一至周日,后端负责生成号源,前端只看展示。前端真需要判断星期,用getDay()后加 1 再比较。这条写在接口文档第一行,能省掉大量无意义调试。
5.5 Navicat 导入 SQL 脚本报错与数据库连接池超时
现象:把项目里的 .sql 文件导入新环境,报错一堆;或者系统跑一会儿后接口变慢,报连接池获取连接超时。
原因:导入报错常见是脚本有外键且表顺序不对、字段用了关键字、字符集没指定。连接池超时常见是 MySQL wait_timeout 默认 8 小时,连接空闲后被服务端断开,连接池还拿着旧连接。
解决:SQL 脚本按第 2 章写法:不建外键、表名和字段名避开 order、group 等关键字、文件头显式写DEFAULT CHARSET=utf8mb4。连接池配置把 max-lifetime 设为 60 秒,小于 MySQL 的 wait_timeout,同时开启 connection-test-query 保活检测,代码里不要自己 new Connection,全部走连接池。
提示:连接池超时报错信息通常是
Connection is not available, request timed out after 30000ms。先查连接池配置,再查 MySQL wait_timeout,这个排查顺序别反过来,最常见原因是 max-lifetime 大于 wait_timeout,连接在池里已经死了。
6. 论文、答辩与验证:把源码和数据库变成能讲清楚的项目
6.1 论文结构怎么排
拿到源码和数据库后,论文顺序我建议:需求分析(背景加用例图)、系统设计(架构图加 ER 图)、数据库设计(表结构加字段说明)、系统实现(核心功能截图加关键代码)、系统测试(功能测试用例表加并发测试截图)。ER 图可以从 Navicat 模型功能直接导出,但论文里一定要把 appointment 和 schedule 的关系线画清楚,这张图是答辩老师第一眼会看的图。
6.2 答辩必问的三个问题
并发与超卖怎么解决:把 FOR UPDATE 和乐观锁的思路讲透。状态流转向量:把 0 待支付到 4 爽约的状态流转背出来,重点解释为什么退号是改状态而不是删记录。权限与安全:区分用户端和管理员端,用户端用 token 鉴权,管理员账号单独做权限校验,不能只靠前端隐藏入口。
6.3 用 Postman 和压测数据给系统验证
答辩前我一定做的一件事:用 Postman 跑通完整流程,登录、科室、医生、号源、预约、退号。然后把并发预约的截图放进测试章节:Postman runner 配置 50 个并发请求,打到同一个号源,最终只有约定数量的请求成功,旁边标注“剩余号源为 0,无超卖”。这张截图的说明力大于一整章文字。
这套源码和数据库,交付的不是几份文件,而是一个你能完整讲清楚的系统。我自己的习惯是答辩前一周不看代码,在纸上把订单状态流转和各表关联画一遍,再自己写一遍。画得出来,答得出来,项目就是你的。希望帮到你。
本文还有配套的精品资源,点击获取