news 2026/9/16 21:42:12

智慧医疗预约挂号Android端设计与高并发实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
智慧医疗预约挂号Android端设计与高并发实践

简介:基于Android的智慧医疗预约挂号系统设计项目,面向毕业设计、课程设计与期末大作业场景,适合Android开发学习者参考,涵盖从需求分析到界面实现与接口交互的完整流程。压缩包共114个文件,大小约4.86MB,包含java源码、xml布局、gradle构建脚本、项目报告docx及图片资源等,目录组织便于直接阅读与调试。目前已有33人学习下载,适合需要快速搭建医疗预约类App框架的读者。项目采用MVC模式构建Android客户端,配合Java后端与MySQL数据库,实现注册登录、挂号预约、排班查看、支付与消息推送等功能,并强调HTTPS传输与数据加密。附带的项目报告docx详细记录了需求分析、数据库设计、接口设计与测试用例,可帮助理解整套系统的前后端协作方式,也能为同类设计报告提供写作思路。

1. 为什么智慧医疗预约挂号的 Android 端要单独做设计

早上八点整,门诊楼的号源池被瞬间清零,后台预约接口十秒内被同一个科室的请求打满——这是智慧医疗预约挂号系统上线后遇到的第一道坎。基于 Android 的智慧医疗预约挂号系统设计,核心不只是把窗口挂号搬到手机上,而是要在放号时段承受高并发的同时,保证同一个号不会被两个人挂走,让患者端看到的科室列表、排班日期和余号数字始终与后台一致。

这套设计覆盖「号源数据、后端服务、Android 客户端」整条链路,客户端决定了大部分体验:页面加载快不快、弱网下会不会重复扣费、余号和后台能不能对得上。适合正在做医疗信息化项目的 Android 工程师,也适合拿这个课题做系统设计或课程作业的开发者。方案不绑定特定云厂商,后端接口协议定好之后,用 Android Studio 加一组常规 REST 服务就能落地,技术栈横向怎么选都不影响主链路。

2. 号源排班的数据模型与并发控制设计

预约挂号系统的命门是「号源」这个有限资源。平时一个科室一天几百个号,谁来谁有,系统没有压力;放号时间一到,同一个排班的请求会在几秒内被几千次命中。这里最忌讳的设计,是把「医生一天有 30 个号」直接做成 doctor 表上的一个数字字段,每次挂号就去 update 这个数字。因为同一行会被大量事务争抢行锁,连接池很快被打满,而且根本无法回答「上午还剩几个号」这类患者真正关心的问题。

2.1 号源模型:排班、号段与号源的三层结构

常见做法是把号源拆成三层:排班描述「哪个医生哪天出诊、总量多少」,号段描述「上午还是下午」,号源描述「具体第几个位置」。这样拆有三个直接好处。第一,余号统计能精确到号段,患者看到日历上每个日期同时显示上午和下午的余号,而不是一整天一个笼统数字。第二,锁的粒度变细,同一医生的并发压力分散到多个号段上,而不是整个医生一天的所有号一起被锁。第三,后续要做「医生停诊换号」「指定时段优先」时,只需要调整排班状态,不用动预约记录。

很多项目原型阶段图省事,一张表同时存排班和余号,等要加「取消预约回补号源」时才发现要同时改三个接口的 SQL。模型拆开后,回补只是把排班的余数字段加回去,再把预约记录改成已取消,两步在同一个事务里完成,逻辑清楚很多。

2.2 预约挂号核心表结构与字段约束

2.2.1 排班表与号源表怎么建

排班表是整套系统的核心表:

CREATE TABLE schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, doctor_id BIGINT NOT NULL, work_date DATE NOT NULL, period TINYINT NOT NULL COMMENT '1-上午 2-下午 3-晚间', total_count INT NOT NULL DEFAULT 0, remain_count INT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 0 COMMENT '0-正常 1-停诊', UNIQUE KEY uk_doc_date (doctor_id, work_date, period) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

两条约束最关键。uk_doc_date唯一索引保证同一医生同一天同一号段只有一条排班记录,这是防重复排班的第一道防线。version字段留给乐观锁,扣减号源时会用到。total_count 和 remain_count 分开存,避免每次扣减都从总量反推,也方便审计放号量调整。排班数据通常由后台提前一周生成,生成时要处理节假日规则和医生停诊安排,客户端只查 status=0 的记录,再按日期分组渲染。日期用 DATE 类型,不要用时间戳替代,跨时区计算在医院场景里只会添乱。

2.2.2 预约记录表和幂等键

患者点「确认挂号」后落库的是预约记录:

CREATE TABLE register_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, patient_id BIGINT NOT NULL, schedule_id BIGINT NOT NULL, slot_no INT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0-待支付 1-已支付 2-已取消 3-已完成 4-已退号', idempotent_key VARCHAR(64) NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_idem (idempotent_key), UNIQUE KEY uk_sched_slot (schedule_id, slot_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

uk_sched_slot是业务上最硬的约束:同一排班下的同一个号位只能被一个人预约成功,即便上层代码出了 bug,数据库也会挡下重复挂号。idempotent_key是客户端生成的唯一请求标识,和「用户连点两次确认」直接相关,生成规则一般是时间戳加用户 ID 加随机数做哈希,长度 32 位足够。status 的状态流转要收敛,散落的魔法数字是后期改不动代码的根源:

status含义允许的下一状态
0待支付1 已支付、2 已取消
1已支付3 已完成、4 已退号
2已取消
3已完成

状态变更全部由服务端 service 层处理,Android 客户端只根据接口返回码判断下一步跳支付页还是提示成功。

2.3 并发挂号的两种锁策略

2.3.1 乐观锁扣减号源

放号瞬间同一排班被大量并发命中,最简单的防超卖是乐观锁,一条 SQL 完成:

UPDATE schedule SET remain_count = remain_count - 1, version = version + 1 WHERE id = #{scheduleId} AND remain_count > 0 AND version = #{oldVersion};

执行后判断受影响行数:为 1 说明扣减成功,继续创建预约记录;为 0 说明余号已空或版本号被抢先更新,直接返回「号源不足」。remain_count > 0是最后一道闸,即使版本号判断被绕过,数据库也不会把余号扣成负数。单库单表部署时,乐观锁配合唯一索引已经能覆盖绝大多数挂号系统的并发量。

2.3.2 Redis 分布式锁处理跨节点竞争

乐观锁的问题在于:扣号源、创建预约记录、生成待支付订单,三步在同一个事务里要连续操作多张表。行锁在高峰期会带来大量锁等待和死锁重试,最终一致性能保证,但接口响应时间明显变长。所以多实例部署时,会再给「提交预约」加一把分布式锁:

String lockKey = "lock:schedule:" + scheduleId; String requestId = UUID.randomUUID().toString(); Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, 5, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { try { // 扣减号源 + 创建记录 + 生成订单,同一个事务 } finally { if (requestId.equals(redisTemplate.opsForValue().get(lockKey))) { redisTemplate.delete(lockKey); } } }

锁粒度是单个排班,不是整个医生或科室,不同医生的预约请求互不阻塞。过期时间设 5 秒,超时直接走「系统繁忙」提示,客户端不要无限重试。释放前先比较 value 再删除,防止锁过期后被别的请求设置、误删别人的锁。乐观锁和分布式锁不是二选一:单机用乐观锁足够,多实例就在乐观锁外面包一层 Redis 锁,把对同一排班的写操作串行化,用一条配置开关控制是否启用,压测后决定上线参数。

3. Android Studio 里的客户端分层与接口对接

客户端在 Android Studio 里组织工程,目标是不引入重型框架,让「页面、状态、数据」三条线互相解耦。医疗项目的 Android 端有几个现实约束:患者手里的手机配置参差,医院内部 Wi-Fi 质量时好时坏,排班数据查询频繁但余号必须实时。这些约束直接决定技术选型。

3.1 工程结构与 Gradle 依赖怎么选

客户端采用 MVVM 加 Kotlin 协程,界面用 XML 加 RecyclerView,图片加载用 Glide,Room 只缓存科室名称这类低频数据,排班和余号一律实时拉取。排班不做本地持久化的原因很简单:余号是强实时数据,缓存几分钟就会让患者到医院才发现号已经被挂完,投诉全部打到导诊台。

android { namespace "com.hospital.register" compileSdk 34 defaultConfig { applicationId "com.hospital.register" minSdk 23 targetSdk 34 versionCode 1 versionName "1.0.0" } } dependencies { implementation 'androidx.core:core-ktx:1.13.1' implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7' implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'com.squareup.retrofit2:retrofit:2.11.0' implementation 'com.squareup.retrofit2:converter-gson:2.11.0' implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0' implementation 'com.github.bumptech.glide:glide:4.16.0' }

minSdk 23 对应 Android 6.0,覆盖医院里还在服役的旧机型;compileSdk 用 34,对应 Android SDK 34,追新没有实际收益。Retrofit 和 OkHttp 选经过验证的版本号,不要用还在 RC 阶段的构建。协程依赖不需要单独加,lifecycle-viewmodel-ktx 已经带上了 kotlinx-coroutines 的传递依赖。

3.1.1 一个文件一段职责

View 层只做两件事:把 ViewModel 的状态渲染到列表,把用户点击转发给 ViewModel。ViewModel 层持有页面全部状态,用密封类描述加载中、成功、失败三种状态。Repository 层只做数据获取,不关心界面。这个分层对挂号场景特别重要,因为页面状态多——加载中、余号不足、网络异常、重复提交——如果状态判断散落在 Activity 里,排障时根本不知道当前页面的状态是哪个分支赋上去的。

3.2 Retrofit 接口设计与统一响应体

接口协议是这套系统里最容易扯皮的部分,我习惯先定四个约定:成功码统一 0;业务错误用 4100 段业务码;HTTP 状态码只承担传输层语义;列表接口统一 page/pageSize 分页。这样 Android 端拦截器只需要处理 401 和 5xx,其余全部走业务码分支。

data class ApiResponse<T>( val code: Int, val message: String, val data: T? ) interface RegisterApi { @GET("api/departments") suspend fun getDepartments(): ApiResponse<List<Department>> @GET("api/schedules") suspend fun getSchedules( @Query("doctorId") doctorId: Long, @Query("date") date: String ): ApiResponse<List<ScheduleVO>> @POST("api/appointments") suspend fun createAppointment( @Body request: AppointmentRequest ): ApiResponse<AppointmentResult> }

挂起函数配合协程,天然避免回调地狱,也方便在 Repository 层做统一的异常转译。业务码在各自端定义成常量,避免魔法数字,常用的几组:

业务码含义客户端动作
0成功按业务跳转
4101号源不足提示并刷新余号
4102重复预约展示已有预约记录
4103排班已停诊返回科室列表
3.2.1 OkHttp 超时与公共拦截器

医院 Wi-Fi 丢包率高,超时参数不能照抄普通互联网应用。连接超时 10 秒、读写超时 15 秒是比较合适的区间,再长用户会以为应用死了,再短弱网下几乎不可用。

val okHttpClient = OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(15, TimeUnit.SECONDS) .writeTimeout(15, TimeUnit.SECONDS) .addInterceptor { chain -> val request = chain.request().newBuilder() .header("Authorization", "Bearer ${TokenStore.get()}") .header("X-Client-Type", "android") .build() chain.proceed(request) } .build()

拦截器统一附加登录态和客户端标识,后端可以按客户端类型做版本控制和调用统计。X-Client-Type 头比 User-Agent 好解析,不用维护一套正则。

3.2.2 401 后的 token 刷新与重放

登录态过期是挂号过程中最常被忽略的异常路径。患者选了半天科室,提交时突然 401,如果直接踢回登录页,体验很差。常见做法是在拦截器里捕获 401,刷新 token 后重放一次原始请求:

class AuthInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request().newBuilder() .header("Authorization", "Bearer ${TokenStore.get()}") .build() val response = chain.proceed(request) if (response.code == 401 && TokenStore.refresh()) { response.close() return chain.proceed(chain.request().newBuilder() .header("Authorization", "Bearer ${TokenStore.get()}") .build()) } return response } }

TokenStore.refresh() 内部要加锁或串行化,否则多个并发请求同时收到 401,会触发多次刷新,后端刷新接口得做防重。重放仍失败就清登录态回登录页,这是最后兜底。

3.3 余号查询的缓存策略

OkHttp 的默认缓存策略可能命中过期的响应,余号接口必须走网络。做法是在响应拦截器里统一写入 Cache-Control 头,禁用本地缓存:

.addInterceptor { chain -> val response = chain.proceed(chain.request()) response.newBuilder() .header("Cache-Control", "no-cache") .build() }

需要注意这个头只对「余号」这类强实时接口生效,科室列表这种低频数据不建议禁缓存。区分方式可以按 URL 路径判断,或者后端在响应头里显式声明,客户端不做猜测。

4. 号源日历与预约提交的代码落地

前两章把模型和客户端骨架搭好,这一章落到患者真正操作的三个页面:科室列表、号源日历、提交预约。这三段代码是整套系统里最容易被问「能不能这样写」的部分,边界条件特别多。

4.1 科室列表和排班数据的加载链路

4.1.1 用 Sealed Interface 管理加载状态

页面状态用密封接口定义,比用多个 LiveData 拼状态清晰得多:

sealed interface HomeUiState { data object Loading : HomeUiState data class Success( val departments: List<Department>, val scheduleMap: Map<LocalDate, List<ScheduleVO>> ) : HomeUiState data class Error(val message: String) : HomeUiState } class HomeViewModel( private val repo: RegisterRepository ) : ViewModel() { private val _uiState = MutableStateFlow<HomeUiState>(HomeUiState.Loading) val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow() fun loadHome() { viewModelScope.launch { _uiState.value = HomeUiState.Loading runCatching { repo.loadHome() } .onSuccess { _uiState.value = HomeUiState.Success(it) } .onFailure { e -> _uiState.value = HomeUiState.Error(e.message ?: "网络异常,请稍后重试") } } } }

StateFlow 保证界面在旋转后能拿到当前状态,不会因为 Activity 重建重新触发网络请求。runCatching 捕获所有异常并转成 Error 状态,UI 层根据状态决定显示加载动画还是错误提示加重试按钮。

4.1.2 日期数据与号源合并

排班页需要一个滚动日期条,常见逻辑是展示从今天起 14 天的窗口,医院一般允许提前 7 到 14 天预约:

fun buildDateList(start: LocalDate, days: Int): List<DateItem> = (0 until days).map { offset -> val date = start.plusDays(offset.toLong()) DateItem( date = date, label = if (offset == 0) "今天" else "周${date.dayOfWeek.getDisplayName(TextStyle.NARROW, Locale.CHINA)}", day = date.dayOfMonth, remainCount = scheduleMap[date]?.sumOf { it.remainCount } ?: 0 ) }

合并逻辑是纯内存操作:后端一次返回 14 天的排班列表,客户端按日期分组,余号是当天所有号段 remainCount 之和。查无记录的日期余号为 0,日期条目置灰不可点。这里有一个容易踩的坑:不要为了「显示好看」把余号为 0 的日期隐藏,患者需要看到哪天有号、哪天没号,隐藏会让人以为系统坏了。

4.2 号源选择的交互状态保存

患者选中某个日期和号段后,旋转屏幕或应用被系统回收,选中状态不能丢。存放位置应该是 ViewModel 而不是 Adapter 或 Activity 的字段:

private val _selection = MutableStateFlow<Pair<LocalDate, Int>?>(null) val selection: StateFlow<Pair<LocalDate, Int>?> = _selection.asStateFlow() fun selectPeriod(date: LocalDate, period: Int) { _selection.value = date to period }

每次点击日期或号段,先更新 StateFlow,UI 根据 StateFlow 变化刷新选中高亮。提交按钮的可用状态也由它驱动,避免出现「选号段之前就能点提交」的非法状态。

4.3 提交预约:幂等键、失败重试与按钮防抖

4.3.1 幂等键的生成与复用

提交预约是整套系统里唯一需要严格幂等的操作。患者的「确认」按钮在弱网下可能发出两次请求,核心是幂等键的生成时机和复用:

suspend fun createAppointment(req: AppointmentRequest): Result<AppointmentResult> { val idempotentKey = UUID.randomUUID().toString() try { val resp = api.createAppointment(req.copy(idempotentKey = idempotentKey)) return if (resp.code == 0) Result.success(resp.data) else Result.failure(BusinessException(resp.code, resp.message)) } catch (e: IOException) { return Result.failure(e) } }

幂等键在第一次提交时生成,应用进程内保存。用户点「重试」时复用同一个 key,不能重新生成——第一次请求可能其实成功了,只是响应丢了,重试如果换 key 会导致挂两次号。服务端靠 register_record 表上的 uk_idem 唯一索引去重,第二次请求直接返回第一次的结果。业务错误和网络错误要分开处理:4101 号源不足属于业务错误,提示后刷新余号,不重试;IOException 可以提示重试,因为请求状态未知。

提示:幂等键的生成放在业务动作开始处,不要在拦截器里给所有请求统一生成。GET 请求用不上,POST 请求也不是每个都需要幂等。

4.3.2 按钮防抖与支付回跳

提交按钮的防抖不止是「禁用 500 毫秒」,而是进入提交中状态后一直禁用,成功跳转或失败恢复时才重新可用:

btnSubmit.isEnabled = false viewModel.submit() // StateFlow 驱动 loading 状态 // 失败回调里恢复 btnSubmit.isEnabled = true

提交成功拿到的是订单号,客户端拉起收银台。这里有一条硬规矩:预约状态以后端回调为准,支付宝或微信的客户端支付成功回调只能作为提示「已支付」的参考,不能用来更新预约记录状态。支付完成回到应用通过 scheme 深链或应用间拉起回调处理,Activity 要配 launchMode 防止支付页回来时重建堆栈。

5. Android 客户端上线前必查的细节与验收方法

客户端功能跑通不等于能上线。按下面三个顺序检查,能把大多数线上客诉挡在发版之前。

5.1 列表页的图片与内存开销

医生头像来自医院图片服务器,很多没有自动裁剪缩略图的能力,原图可能两三兆。Glide 必须显式限制尺寸和格式:

Glide.with(itemView) .load(doctor.avatarUrl) .override(120, 120) .format(DecodeFormat.PREFER_RGB_565) .into(ivAvatar)

override 强制按 120 像素解码,RGB_565 比 ARGB_8888 少一半内存,头像这种没有渐变透明需求的图片看不出差异。科室列表几十个头像,区别不大;但患者翻页时 RecyclerView 会反复复用 item,不做限制很容易出现滚动卡顿。

5.2 用真机模拟连点与弱网

重复提交的验证不能只靠手点,用 adb 模拟连点更接近真实误触:

# 连续点击预约按钮 50 次,坐标按真机分辨率换算 adb shell "for i in $(seq 1 50); do input tap 540 1200; sleep 0.05; done"

跑完去服务端查 register_record 表,按 idempotent_key 分组统计,每个 key 的记录只能有一条。弱网场景直接在 Android Studio 里给模拟器设置网络丢包和延迟档位,验证两个行为:超时后页面有重试入口,重试请求返回的是第一次的预约结果而不是报「重复预约」。这两条过了,线上最常见的两类投诉——「扣了两次钱」和「明明挂上了说没挂上」——基本能杜绝。

5.3 一组可执行的验收清单

检查项方法通过标准
并发不超卖100 并发请求同一排班成功数等于余号数,无负数
重复提交幂等adb 连点 50 次同一幂等键仅一条记录
弱网超时模拟 50% 丢包提示重试,不崩溃不卡死
旋转屏状态选中号段后旋转选中状态保留
内存占用Profile 连续翻页 5 分钟无持续增长的 GC 波动

清单里的并发项建议直接写个脚本打真实接口,不要用模拟器里的并发测试,模拟器网络栈和真机差异太大。adb 脚本里的 tap 坐标记得按目标真机分辨率换算,不同机型按钮位置不同,脚本跑的时候全程盯着画面确认点在按钮上。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 21:42:01

示波器假故障排查指南:识别触发与探头设置陷阱

上周同事火急火燎地跑来&#xff0c;说实验室那台示波器“坏了&#xff0c;波形缩成一团&#xff0c;还满屏乱跳”。我过去扫了一眼显示界面&#xff0c;探头倍率菜单停在“10X”&#xff0c;探头硬件开关却拨在“1X”——这种“假故障”&#xff0c;我一年能遇上几十次。说句不…

作者头像 李华
网站建设 2026/9/16 21:41:53

华为nova 8 Pro固件zip验证、解包与刷机全流程指南

简介&#xff1a;面向华为nova 8 Pro用户和Android开发者的系统级资源包&#xff0c;内含与设备固件更新、模块化配置及自动化安装相关的核心文件&#xff0c;适合需要刷机、系统优化或定制开发的人群。压缩包共7个文件&#xff0c;以shell脚本&#xff08;3个&#xff09;、pr…

作者头像 李华
网站建设 2026/9/16 21:40:22

开源相册分享小程序配独立后台:从本地复现到上线部署实践

简介&#xff1a;一款开源版酷炫相册分享小程序源码&#xff0c;含独立后台&#xff0c;适合小程序开发者、独立站长和内容运营者使用&#xff0c;可用于搭建个人相册、作品展示、付费分享类小程序。源码在官方开源版基础上进行解密与功能增强&#xff0c;覆盖相册管理、访问密…

作者头像 李华
网站建设 2026/9/16 21:39:34

PHP反序列化漏洞实战:字符串逃逸与session注入绕过过滤

这道题我在BUUCTF上刷的时候卡了挺久&#xff0c;不是因为反序列化本身多难&#xff0c;而是入口藏得比较深&#xff0c;加上filter会把关键词替换成空字符串&#xff0c;导致序列化数据长度对不上&#xff0c;直接unserialize就炸。后来把源码审计思路捋顺之后发现&#xff0c…

作者头像 李华
网站建设 2026/9/16 21:38:00

图像去噪技术:7种经典算法原理与Matlab实现

1. 图像去噪技术概述图像去噪是数字图像处理中最基础也最关键的预处理步骤之一。作为一名长期从事医学影像处理的工程师&#xff0c;我深刻理解噪声对后续分析&#xff08;如病灶识别、三维重建&#xff09;的灾难性影响。在实际项目中&#xff0c;我们往往需要根据不同的噪声特…

作者头像 李华