十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

预约挂号APP开发实战:数据库表结构设计、防超卖与Android接口联调

预约挂号APP开发实战:数据库表结构设计、防超卖与Android接口联调 简介这是一套基于Android的预约挂号APP毕业设计项目采用前后端分离架构后端可选用SpringBoot/SSM框架前端使用Android原生开发数据库为MySQL 5.7适合计算机相关专业学生用于毕业设计、课程设计或期末大作业。资源包共6个文件、24.63MB包含项目源码、数据库SQL脚本、部署说明TXT文档及两份PPT答辩演示文稿覆盖从环境搭建到项目运行的完整流程。项目经过严格调试确保代码可运行且注释详细新手也能快速理解。除Android端源码外还附带微信小程序版本的预约挂号系统演示文稿便于横向对比或作为答辩展示素材。部署说明中列出了JDK、IDEA、AndroidStudio等环境的配置要点并建议将Gradle下载源改为国内镜像以提升构建速度。目前已有406人学习适合需要快速落地或参考完整项目结构的同学直接使用。1. 一个预约挂号APP的毕设到底在考察什么预约挂号APP在学生毕业设计里出现频率很高但它并不是“做几个列表页然后跳转”的产品题。用户能感知的动作是“选医院、选科室、看医生排班、选时段、填信息、提交成功”而真正决定项目能不能在答辩现场讲清楚的关键是医院、科室、医生、排班、号源、订单这几类数据之间的关系以及“两个用户同时抢最后一个号”时系统如何保证不超卖。本文以一个可落地的预约挂号APP为例从服务端表结构、接口规范到 Android 网络层实现一步步拆解。适合正在做 Android 毕业设计、手里只有部分源码和数据库脚本、需要把项目串成完整方案的同学。2. 预约挂号APP的技术选型与工程骨架Android端 服务端接口2.1 不要把预约挂号做成单机版数据库一定要放在服务端预约挂号这个业务天然是“多端共享一套数据”的系统。手机A预约了号手机B在同一时刻应该看到号源减少而不是各存一份本地数据。这一点也是毕业答辩里最容易翻车的地方只给 Android 工程嵌一个 SQLite 或 Room 数据库所有预约记录都存在手机本地老师打开模拟器换一个账号登录之前的数据就“丢”了。所以常规做法是三层结构Android 客户端做展示和交互服务端提供 REST 接口MySQL 负责持久化数据库脚本独立存放在项目里的 sql 目录。如果你手头拿到的 Zip 里只有 Android 源码和一个 .sql 文件那说明数据库脚本是给服务端用 MySQL 还原的。Android 端不要直接连 MySQL也不要在客户端写jdbc:mysql://这种连接串。客户端通过 Retrofit 或 OkHttp 访问服务端接口拿到 JSON 数据再渲染。这样做的好处是表结构变更时只需要改服务端接口返回字段不用重新发版本坏处是你得多写一个小的服务端工程但毕业设计阅卷时这反而是加分项。真机调试时需要注意Android 模拟器访问宿主机服务端不要用localhost要用http://10.0.2.2:8080/这个地址被模拟器固定映射到宿主机。真机调试则需要把地址换成电脑在局域网里的 IP并且保证手机和电脑连同一个 WiFi。这个坑我在帮人调项目时几乎每次都遇到页面一直 loading抓包一看全是ConnectException把 BaseUrl 改对就通。2.2 Android 端选型Retrofit Gson RecyclerView Glide做预约挂号APPAndroid 端不需要引入特别复杂的架构。MVVM、Hilt 这类框架当然可以写但对毕业设计来说把功能跑通、把代码结构讲清楚更重要。最稳妥的组合是Retrofit 做网络请求Gson 做 JSON 解析RecyclerView 做列表Glide 加载医生头像SharedPreferences 或本地数据库存登录态。这个组合在 Android Studio 里创建新项目后加几个 Gradle 依赖就能用不需要配置乱七八糟的编译器参数。定义一个医院列表接口常见写法如下public interface HospitalApi { GET(hospital/list) CallApiResultListHospital listHospital(); GET(schedule/list) CallApiResultListScheduleVO listSchedule( Query(deptId) long deptId, Query(date) String date); }接口定义中GET后面写的是相对路径完整的请求地址由 BaseUrl 拼接而成。ApiResultT是一个统一的响应包装类正常情况下包含code、msg、data三个字段所有接口都返回这个结构Android 端解析时就只需判一次code 0即可。Query注解用来拼接查询参数listSchedule对应的请求就是/schedule/list?deptId1date2025-04-10这种形式服务端按这两个参数去查排班表。访问服务端时Android 默认禁止在主线程做网络操作所以调用 Retrofit 的call.enqueue()是标准姿势回调会切到主线程执行可以直接更新 UI。如果项目中看到的还是new Thread()加runOnUiThread()的写法那是老项目同样的逻辑用 Retrofit 写会省掉一半代码。2.3 Zip 包里“源码 数据库”的常见工程结构一个完整的预约挂号毕业设计 Zip解压后通常是这样的布局Android 工程目录占主体里面有app/src/main/java放 Java 或 Kotlin 源码res放布局和图片资源build.gradle管理依赖版本服务端源码可能是独立的 Spring Boot 工程或一个server文件夹数据库脚本一般放在db/hospital.sql或doc/hospital.sql。有的压缩包只给出 Android 端源码和一份创建表的 SQL这时候服务端需要自己补。拿到项目后不要急着打开 Android Studio先把 SQL 脚本过一遍。看表结构里有没有t_hospital、t_department、t_doctor、t_schedule、t_timeslot、t_order这类核心表。如果只有用户表和预约表那说明数据模型是简化过的页面上的医院和科室很可能是写死在 Android 代码里的静态数组。能用是能用但答辩时经不起追问“如果换一家医院怎么办”。我的建议是哪怕服务端代码不写也先把六张核心表建全用最简单的 Servlet 或 Spring Boot 把它们查出来返回 JSON整个项目的完成度会明显不一样。3. 预约挂号核心表结构设计与数据库防超卖SQL3.1 基础实体用户、医院、科室、医生预约挂号这个业务先有“医院—科室—医生”这个层级前置表不要一上来就把字段堆在订单表里。用户表保存账号和就诊人基本信息医院表、科室表、医生表逐级外键关联。建表脚本如下可以直接作为数据库课程设计的起点CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, phone VARCHAR(16) NOT NULL, password_hash CHAR(64) NOT NULL, name VARCHAR(32) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_phone (phone) ); CREATE TABLE t_hospital ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL, level VARCHAR(16) COMMENT 三级甲等/二级甲等, address VARCHAR(128) ); CREATE TABLE t_department ( id BIGINT PRIMARY KEY AUTO_INCREMENT, hospital_id BIGINT NOT NULL, name VARCHAR(64) NOT NULL, intro TEXT, KEY idx_hospital (hospital_id) ); CREATE TABLE t_doctor ( id BIGINT PRIMARY KEY AUTO_INCREMENT, dept_id BIGINT NOT NULL, name VARCHAR(32) NOT NULL, title VARCHAR(32) COMMENT 主任医师/副主任医师, avatar_url VARCHAR(255), intro TEXT, fee DECIMAL(10,2) DEFAULT 0 );设计时注意几个点。password_hash用固定长度CHAR(64)对应 SHA-256 的十六进制输出长度密码不要明文存储这是评审老师一定会扫一眼的地方。t_department挂在t_hospital下面同一科室可能存在于多家医院不要在科室表里写死一个医院名。t_doctor挂在科室下fee在医生层设置因为同一科室下不同职称的医生挂号费不同这一点在计算订单金额时要用到。COMMENT注释写清楚字段含义导出 SQL 的时候老师会看表结构注释比字段名更能说明你理解业务。3.2 排班表与号源表医生出诊和号源数量分开建模排班和号源是这个业务里最容易设计混乱的部分。很多初学会把“某医生某天上午出诊”和“上午还剩几个号”混在一张表里字段一会儿加total_count一会儿加remain_count最后排班和号源变成同一份数据。更好的做法是把排班与号源拆成两张表排班表记录医生某天是否出诊、是上午还是下午号源表记录该排班下具体的时间段以及每个时间段的总号和剩余号。CREATE TABLE t_schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, doctor_id BIGINT NOT NULL, dept_id BIGINT NOT NULL, work_date DATE NOT NULL, period TINYINT NOT NULL COMMENT 1上午 2下午, status TINYINT NOT NULL DEFAULT 1 COMMENT 1出诊 0停诊, UNIQUE KEY uk_doctor_date_period (doctor_id, work_date, period) ); CREATE TABLE t_timeslot ( id BIGINT PRIMARY KEY AUTO_INCREMENT, schedule_id BIGINT NOT NULL, start_time TIME NOT NULL, end_time TIME NOT NULL, fee DECIMAL(10,2) NOT NULL, total_count INT NOT NULL, remain_count INT NOT NULL, KEY idx_schedule (schedule_id) );t_schedule上的唯一键(doctor_id, work_date, period)保证同一个医生在同一天同一个午别只能有一条出诊记录重复插入时数据库会直接报错这比在代码里写 if 判断可靠得多。t_timeslot按schedule_id关联排班例如上午排班下面可以生成 08:00–08:30、08:30–09:00、09:00–09:30 三个时段每个时段默认total_count 5remain_count初始等于total_count。页面展示时用户看到的是“医生头像 出诊日期 剩余号数”数据来源就是这两张表的联查。这里有一个值得在代码注释里写清楚的设计意图为什么不直接在t_timeslot里写doctor_id。因为同一个医生换排班后号源是重新生成的保留排班维度的好处是医生停诊时只需要把t_schedule.status置 0该排班下的所有时段自动不可约不需要逐条改号源表。3.3 订单表与状态机待支付、已确认、已取消、已过期订单表承载预约结果也承载状态流转。状态字段用TINYINT不要用字符串VARCHAR存“已预约”这种中文值一是浪费空间二是写代码时容易打错字三是排序和统计都不方便。用数字表示状态在 Java 代码里定义一个枚举或常量类统一对应CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, timeslot_id BIGINT NOT NULL, schedule_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已确认 2已取消 3已完成 4已过期, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), KEY idx_user (user_id), KEY idx_timeslot (timeslot_id) );状态机建议只保留四条迁移路径待支付转已确认用户支付待支付转已取消用户取消待支付转已过期超时未支付由定时任务触发已确认转已完成就诊后标记。不要在代码里允许任意状态互转比如已取消的订单不能再变成已确认。把状态迁移的逻辑收敛到一个 Service 方法里每个方法只做一件事payOrder()、cancelOrder()、expireOrder()、finishOrder()。面试和答辩时这类“状态收敛”的设计比写十个 Activity 更值得讲因为老师关心你是否想清楚了业务边界。订单号不要用数据库自增 id 直接暴露给用户因为自增 id 能看出系统一天的订单量这属于信息泄露。常见做法是生成一个 20 位左右的业务单号日期时间14位 用户ID后四位 随机数补足或者直接用UUID去掉横杠再截断。注意order_no必须建唯一索引这是后面做幂等判断的基础。3.4 防超卖的关键UPDATE 里带 remain_count 0预约挂号的并发问题是毕业设计里含金量最高的知识点。场景是一个时段只剩最后一个号两个用户同时点击提交如果代码逻辑是“先查询剩余号数大于0就扣减再插入订单”在并发下两个请求都可能查到remain_count 1然后都执行插入最后变成超卖。解决的办法是把“检查”和“扣减”合并成一条 SQLUPDATE t_timeslot SET remain_count remain_count - 1 WHERE id #{timeslotId} AND remain_count 0;执行这条 UPDATE 后根据受影响的行数判断是否抢号成功。影响了 1 行说明remain_count在扣减前大于 0扣减成功影响了 0 行说明剩余号数已经是 0用户约满。这个操作依赖 MySQL 行锁两个事务同时执行时会串行化直接避免超卖。配合订单插入放在同一个事务里就形成了“扣号源 写订单”的原子操作。完整下单逻辑在 Service 层看起来是这样Transactional public BookResult tryBookTimeslot(Long timeslotId, Long userId) { // 同一个用户不允许重复约同一个时段 if (orderMapper.countByUserAndTimeslot(userId, timeslotId) 0) { return BookResult.fail(您已预约过该时段请勿重复提交); } // 核心条件写在 SQL 里数据库行锁保证不超卖 int rows timeslotMapper.decreaseRemain(timeslotId); if (rows 0) { return BookResult.fail(该时段号源已满); } Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setTimeslotId(timeslotId); order.setScheduleId(queryScheduleIdByTimeslot(timeslotId)); order.setStatus(0); orderMapper.insert(order); return BookResult.ok(order); }这里有两层防护。第一层查重t_order表上可以再加一个(user_id, timeslot_id)唯一索引数据库层面挡住同一用户重复预约第二层就是这条带条件的 UPDATE防止不同用户超卖。这种“数据库条件更新”的技巧同样适用于秒杀、选课、活动报名等所有余量扣减场景掌握之后在简历上写项目经验时可以理直气壮写上“基于数据库行锁解决并发超卖”。注意 MyBatis XML 里写大于号要转义update iddecreaseRemain UPDATE t_timeslot SET remain_count remain_count - 1 WHERE id #{timeslotId} AND remain_count gt; 0 /updategt;是 XML 中对的转义写法。如果忘了转义直接写XML 解析会报错如果顺手写成了还要转义成gt;否则同样报错。这类细节说出来能给对方“这个人真的调过接口”的感觉而不是照着教程抄了一遍。4. 从科室列表到预约成功Android 与接口对接的完整链路4.1 排班与时段列表的页面数据拼装用户的操作路径是首页选择医院进入医院后展示科室列表点击科室展示该科室下医生的出诊排班选择排班后弹出当天的号源时段列表。服务端在设计接口时不要要求 Android 端自己算“明天是周几、上午下午怎么分”这些判断放在服务端完成Android 端只负责展示。排班列表接口返回的数据结构大致是这样{ code: 0, data: [ { scheduleId: 101, doctorId: 5, doctorName: 王建国, title: 主任医师, avatarUrl: http://xx/avatar/5.png, workDate: 2025-06-10, period: 1, periodText: 上午, timeslots: [ { timeslotId: 501, timeText: 08:00-08:30, remainCount: 5, fee: 20.00 }, { timeslotId: 502, timeText: 08:30-09:00, remainCount: 0, fee: 20.00 } ] } ] }timeslots是内嵌在排班对象里的数组Gson 在解析时不需要额外配置也能处理这种嵌套结构。Android 页面用一个外层RecyclerView展示排班卡片卡片内再嵌套一个RecyclerView横向滚动展示时段这是预约挂号APP最常用的布局。时段剩余为 0 的项要把按钮置灰文案显示“已约满”不要等用户点进去再提示失败这个细节虽然小但很影响体验也属于“边界状态处理”的加分答辩点。列表加载时的空态也要处理。比如“该科室暂无排班”或“该医生今日停诊”不要白屏。用ProgressBar展示 loading数据返回后隐藏请求失败在页面上显示重试按钮而不是直接Toast一下就消失。这个完整链路写清楚约等于把 Android 列表加载的骨架代码全部打通。4.2 提交预约的 Android 实现防重复点击与进度条反馈提交预约是整个 App 最核心的操作。用户选好时段确认订单页展示医生、时间、挂号费点击“提交预约”按钮后按钮置灰显示一个加载进度条同时发起网络请求。请求失败后按钮恢复可点击并给出失败原因请求成功后跳转“预约成功”页显示订单号。重点在于防止用户因为网络慢而疯狂点按钮导致发出多个请求。private void submitBooking(long userId, TimeslotVO timeslot) { btnSubmit.setEnabled(false); progressBar.setVisibility(View.VISIBLE); BookingApi api RetrofitClient.get().create(BookingApi.class); BookRequest request new BookRequest(userId, timeslot.getId()); CallApiResultOrderInfo call api.book(request); call.enqueue(new CallbackApiResultOrderInfo() { Override public void onResponse(CallApiResultOrderInfo call, ResponseApiResultOrderInfo response) { progressBar.setVisibility(View.GONE); btnSubmit.setEnabled(true); ApiResultOrderInfo body response.body(); if (body ! null body.getCode() 0) { startActivity(SuccessActivity.newIntent(context, body.getData())); finish(); } else { Toast.makeText(context, body.getMsg(), Toast.LENGTH_SHORT).show(); } } Override public void onFailure(CallApiResultOrderInfo call, Throwable t) { progressBar.setVisibility(View.GONE); btnSubmit.setEnabled(true); Toast.makeText(context, 网络异常请检查网络后重试, Toast.LENGTH_SHORT).show(); } }); }这段代码里btnSubmit.setEnabled(false)在请求发出前立即执行避免双击。progressBar的可见性切换保障用户能感知请求状态。body.getCode() 0判断的是业务成功HTTP 层面的 200 只代表请求到达了服务端不代表预约成功。这里有一个容易踩的坑服务端可能返回code 500但 HTTP 状态码仍然是 200如果代码里直接拿response.isSuccessful()判断成功就会把失败当成功处理。统一用ApiResult的code字段判断才是正确姿势。OkHttp 的网络超时配置也要检查。默认连接超时是 10 秒在校园网这种弱网环境下预约提交经常出现前端迟迟不回调的问题。通常手动在 Retrofit 构建时设置超时时间推荐配置为connectTimeout(10, TimeUnit.SECONDS)、readTimeout(15, TimeUnit.SECONDS)、writeTimeout(15, TimeUnit.SECONDS)。一个普通提交请求在服务端只需几十毫秒如果 15 秒还没返回大概率是网络断了或者服务端卡死直接提示失败比让用户干等更合理。4.3 取消预约的号源回滚与事务处理取消预约在业务上是下单的逆操作处理不当会出现号源凭空消失。用户取消订单后t_order.status要改成 2已取消同时t_timeslot.remain_count要加 1。这两个操作必须在同一个数据库事务中提交否则可能出现订单已取消但号源没加回去或者号源加回去了订单还是待支付状态。Transactional public void cancelOrder(Long orderId) { Order order orderMapper.selectById(orderId); if (order null) { throw new BizException(订单不存在); } if (order.getStatus() ! 0 order.getStatus() ! 1) { throw new BizException(当前状态不可取消); } orderMapper.updateStatus(orderId, 2); timeslotMapper.increaseRemain(order.getTimeslotId()); }Transactional注解可以放在这个 Service 方法上异常时自动回滚remain_count不会和订单状态不一致。注意increaseRemain的 SQL 是UPDATE t_timeslot SET remain_count remain_count 1 WHERE id #{id}不要受前面防超卖 SQL 的影响也加上remain_count 0的条件取消失败时大概率是某处代码把号源数量改错了排查时先去看订单状态是不是已经被改成已取消。如果项目里还需要支持“医生停诊”场景回滚逻辑会更复杂停诊后已预约该医生号源的用户要收到通知订单批量置为已取消号源不需要恢复因为停诊后号源也不再对外开放。这部分逻辑在毕设里属于锦上添花但可以在论文的“系统功能扩展”章节写一笔。5. 答辩前用自查清单验证预约挂号APP的边界行为5.1 十条必测场景把下面这十种情况逐项在模拟器里跑一遍能显著减少现场演示时的意外场景预期行为自查方法同一用户快速双击提交预约只生成一条订单连点“提交预约”按钮观察订单列表两个账号同时抢最后一个号一个成功一个失败服务端日志看两条请求耗时接近时结果取消后再约同一时段可以成功预约取消后刷新页面号源数量加 1号源为 0 的时段按钮置灰不可点击找一个约满的时段查看 UI 状态订单超过支付时限自动变已过期把定时任务间隔改小等待几分钟观察弱网环境提交预约提示网络异常可重试开启飞行模式后再点提交医院列表为空显示空态和刷新按钮连一个没有医院的测试服务端排班日期过了一天日期列表不出现过去日期切换设备时间测试医生停诊排班从列表消失已约订单取消后台把 schedule status 改成 0未登录直接点预约跳转登录页清除登录态后操作这十条覆盖了并发、状态流转、空态和 UI 反馈四个维度每一类都能对应到上面讲过的设计点。过程中遇到的问题基本都是表结构缺失或状态没有收敛导致的不会牵涉特别高深的技术。5.2 回答“你的系统有什么亮点”的三个角度答辩被问到项目亮点时不要回答“我做了登录注册和预约挂号”。可以从三个具体设计点展开。第一号源扣减使用带条件的 UPDATE 语句利用数据库行锁解决并发超卖问题并且已经在代码里验证过两个并发请求只有一个成功。第二订单状态机显式定义四条迁移路径所有状态变更收敛到 Service 层核心操作全部放在事务中取消了数据和状态不一致的可能。第三客户端做到接口统一返回ApiResult结构业务失败和 HTTP 失败分开判断配合按钮置灰和进度条反馈避免了重复提交订单。这三条每一句都可验证比背十页架构图更容易让老师相信这是你自己写出来的代码。答辩前建议把六张核心表的 ER 图画在一页 PPT 上然后在图上标注事务边界下单时需要同时更新t_timeslot和t_order取消时需要同时更新t_order和t_timeslot。再把状态机的四条迁移路径画在旁边。这份图就是整个预约挂号APP的业务闭环代码、数据库与论文都围绕它铺开比临时翻源码能讲得清楚得多。本文还有配套的精品资源点击获取
返回列表