ARTICLE DETAIL

资讯详情

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

基于Android的智慧医疗预约挂号系统:号源模型与状态机设计

基于Android的智慧医疗预约挂号系统:号源模型与状态机设计 简介基于Android的智慧医疗预约挂号系统设计资源包专供安卓方向毕业设计、课程设计或期末大作业参考。资源以完整项目为主线清晰呈现安卓客户端、Java后端与MySQL数据库协同工作的预约挂号平台涉及MVC分层架构、RESTful风格接口、HTTPS安全传输、消息推送等实用技术点同时附带设计文档中的需求分析、功能划分、数据库表结构、接口规范和测试用例可作为论文撰写与答辩准备的底稿。整个压缩包共114个文件体积约4.86MB包含40个XML界面布局与配置文件、14个Java核心源码、16个PNG和30个JPG图标及素材以及Gradle构建脚本和项目报告docx文档目录结构清晰便于直接导入工程查看和二次开发。目前已有33人浏览学习对于急需一套可运行、可讲解、可扩展的医疗预约项目的学生或开发者能够有效节省从零搭建架构和编写文档的时间。1. 基于 Android 的智慧医疗预约挂号系统设计先解决号源问题把这个题目当成一个普通 App 开发来做是很多人的第一反应但也是拿不到高分的主要原因。基于 Android 的智慧医疗预约挂号系统设计难点不在界面而在号源模型、预约状态机和前后端接口契约。这三个点没想清楚功能写得再全一上线就会被“同一时段两个人预约同一个号”这种问题打穿。这个标题背后是一套典型的分诊流程用户查科室、看医生排班、选择号源、提交预约必要时取消或改签医院侧还要维护排班模板、号源池和停诊规则。适合阅读这篇内容的人有两类一类是把毕业设计或课程设计选成这个题目的学生另一类是刚转到移动医疗方向、想了解业务落地的 Android 工程师。前者建议从第 2 章顺下来踩坑会更少后者可以直接跳到第 4 章看架构。2. 号源模型设计先把“号”这张表建对2.1 先圈业务边界用例图比界面原型更重要我评审这类系统设计时先看用例图不看界面原型。原因是预约挂号涉及的场景非常固定角色也只有患者、医生、管理员三类但用例之间的事件流一旦画错后面的数据库和接口都会跟着错。患者端至少要有这些用例查看科室与医生、按日期或时段查询排班、预约挂号、取消预约、查看预约记录。管理员端要有医生信息维护、排班模板设置、号源释放或锁定。医生的动作相对简单主要是查看当日患者列表和标记出诊状态。事件流上最容易漏的是两个分支停诊处理医生临时停诊后已预约号源怎么转移和超时支付待支付订单超过时限后号源是否立刻释放。把这两个分支写进用例文档再进数据库设计基本不会返工。2.2 排班与号源两级模型医生排班表 时段号源表号源模型的常见做法是拆成两级排班表schedule是医生某天的出诊计划号源表slot才是患者真实去抢的“坑位”。排班表只描述“李医生周二上午出诊限号 30”号源表则把这 30 个坑位拆成一个个可预约的时段。这两张表的粒度差别是关键先看对比。层级表名粒度是否可被预约一级schedule某医生某一天否二级slot某个具体时段是CREATE TABLE schedule ( schedule_id BIGINT PRIMARY KEY, department_id BIGINT NOT NULL, doctor_id BIGINT NOT NULL, work_date DATE NOT NULL, start_time TIME NOT NULL, end_time TIME NOT NULL, fee DECIMAL(10,2) DEFAULT 0.00, status TINYINT DEFAULT 1 ); CREATE TABLE slot ( slot_id BIGINT PRIMARY KEY, schedule_id BIGINT NOT NULL, slot_no INT NOT NULL, start_time TIME NOT NULL, end_time TIME NOT NULL, remaining INT DEFAULT 1, version INT DEFAULT 0 );schedule 表里 status 用于标记停诊1 表示正常2 表示停诊。slot 表的 remaining 默认是 1也就是一个时段只放一个号部分医院会把半小时拆成两个号此时 remaining 改成 2。注意首诊和复诊最好分开。如果不加 patient_type 字段患者会反复挂同一个医生的号业务上很难解释。我一般会把 patient_type 直接放进 slot 表让首诊与复诊各占不同时段。2.3 防超卖的扣减方式带条件更新比事务更可靠预约请求到达后端时常见的防超卖写法是事务里先 SELECT 再 UPDATE但两个并发事务同时读到 remaining1 时都会走更新最终超卖。我在生产环境更偏好一条带条件的 UPDATE由数据库行锁兜底UPDATE slot SET remaining remaining - 1, version version 1 WHERE slot_id ? AND remaining 0;执行后检查影响行数为 1 说明扣减成功为 0 说明号源已被抢空。如果还需要把用户信息写进订单表就把它和 INSERT 放进同一个事务UPDATE 的影响行数作为事务成功的判定条件。乐观锁version 字段在扣号源这里只能作为辅助因为数据库单行更新里UPDATE ... WHERE remaining 0本身已经足够且比每次比较 version 少一次查询。version 更多用于缓存更新场景比如本地缓存与新数据的冲突检测。选型理由很简单号源扣减是高频小事务行锁范围越小并发能力越高。3. 接口契约与预约状态机联调之前先对状态3.1 接口列表先定这些前后端才不会扯皮前后端联调时最怕接口数量临时变。Android 端锁定在七个接口上其余都是加分项。接口方法说明/api/departmentsGET科室列表/api/departments/{id}/doctorsGET某科室下的医生列表/api/doctors/{id}/schedulesGET医生排班日期精确到天/api/schedules/{id}/slotsGET某天排班下的可约号源/api/appointmentsPOST提交预约/api/appointments/{id}GET查询预约详情/api/appointments/{id}/cancelPOST取消预约返回值建议统一包一层。我一般用{ code, message, data }code 为 0 表示成功。预约相关的接口不要用非 200 的 HTTP 状态码来区分业务错误否则 Retrofit 默认会抛 HttpException还得专门去接。业务错误码只出现在 body 里客户端只关心 code。3.2 预约状态机待支付、已支付、已完成预约记录的状态远比想象中多。最少要有 INIT、PAID、DONE、CANCELLED、EXPIRED 五个状态不能只用一个字段简单标记。状态表如下。状态含义允许迁移INIT已提交未支付PAID、CANCELLED、EXPIREDPAID已支付DONE、CANCELLED退号DONE已就诊无CANCELLED已取消无EXPIRED超时未支付无客户端在取消按钮上要判断状态INIT 和 PAID 才能进取消流程DONE 之后只能走退费申诉。后端在取消接口里还要判断当前时间距预约时段是否小于某个阈值我一般限制在开诊前 30 分钟以上才能取消否则号源释放后人赶不过来。停诊是状态机里最容易漏的分支。医生停诊时已 PAID 的订单不能直接 CANCELLED一般走 REFUNDING 状态由运营确认后退款并释放号源。Android 端在这时要展示“已停诊退款处理中”而不是简单显示“已取消”。3.3 幂等键重复提交最后的防线移动端最常见的问题是用户连续点了两次“提交预约”产生两笔订单。这个问题的标准解法是客户端生成 clientToken服务端用它在 24 小时内对同一请求去重。请求体里带上它即可。{ slotId: 10086, patientType: FIRST_VISIT, patientId: 20240001, clientToken: 7f3a9c21-6b8e-4d2f-9a31-c0f4b2d8e1a5 }服务端把 clientToken 作为唯一键建索引收到重复请求直接返回第一次的订单结果而不是报错。这个字段对客户端来说就是一个普通 UUID生成后跟随整个预约流程。设计文档里必须写明这套规则否则测试环节永远在追重复单问题。4. Android 端实现包结构、网络层与预约页4.1 按 feature 分包不按 layer 分包Android Studio 新建项目时默认按 layer 分包activity、adapter、model这个结构对预约挂号这种多业务 App 非常难维护。我一般改成 feature 分包一个业务闭环一个包。. ├── app/ │ ├── data/ # 网络、数据库、Repository │ │ ├── api/ # Retrofit 接口定义与拦截器 │ │ ├── db/ # Room 数据库 │ │ └── repository/ # 数据仓库 │ ├── feature/ │ │ ├── home/ # 首页科室入口 │ │ ├── doctor/ # 医生列表与排班 │ │ ├── booking/ # 预约提交与支付状态 │ │ └── mine/ # 我的预约、退号 │ ├── common/ # Loading、Toast、工具类 │ └── di/ # 手动注入或 Hilt 配置feature 包内的每个一级目录理论上可以单独拆成模块但课程设计阶段没必要拆 modulePackage-by-Feature 已经足够。di 目录放一个简单的 ServiceLocator 也行不一定要上 Hilt关键是让 ViewModel 不自己 new Repository。4.2 Retrofit 网络层封装把线程切换收敛起来网络层常见做法是用 Repository 暴露挂起函数ViewModel 不直接碰 Retrofit。下面这段依赖 Retrofit 的 suspend 写法可以省掉大部分 RxJava 样板代码。interface AppointmentApi { GET(departments) suspend fun getDepartments(): ApiResponseListDepartment GET(doctors/{doctorId}/schedules) suspend fun getSchedules( Path(doctorId) doctorId: Long, Query(date) date: String ): ApiResponseListSchedule POST(appointments) suspend fun createAppointment( Body request: CreateAppointmentRequest ): ApiResponseAppointment }ApiResponse 就是前面约定的{ code, message, data }外壳。Retrofit 的 suspend 方法会把网络请求切到 IO 线程返回后自动回到主线程ViewModel 里不需要再写 Dispatchers.IO。一个容易踩的坑是超时参数。OkHttp 默认连接超时 10 秒但预约提交涉及后端库存扣减和支付回调读超时建议调到 30 秒以上。val client OkHttpClient.Builder() .connectTimeout(15, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .addInterceptor(AuthInterceptor(tokenProvider)) .build()连接超时可以短一些15 秒足够读超时给到 30 秒是为了让后端有足够时间完成号源锁定与支付回调校验。写超时也是 30 秒上传请求体时不会被弱网打断。4.3 医生列表页StateFlow 两级加载医生列表是典型的“先科室后医生”两级页面。套路是第一级加载科室列表点击科室后带着 departmentId 请求医生列表在 ViewModel 里用一个 UiState 承载界面状态。data class DoctorUiState( val loading: Boolean false, val doctors: ListDoctor emptyList(), val error: String? null ) class DoctorViewModel( private val repo: AppointmentRepository ) : ViewModel() { private val _uiState MutableStateFlow(DoctorUiState()) val uiState: StateFlowDoctorUiState _uiState.asStateFlow() fun loadDoctors(departmentId: Long) { viewModelScope.launch { _uiState.value DoctorUiState(loading true) runCatching { repo.getDoctors(departmentId) } .onSuccess { _uiState.value DoctorUiState(doctors it) } .onFailure { _uiState.value DoctorUiState(error it.message) } } } }这里用 StateFlow 而不是 LiveData是因为集合类型的数据用 LiveData 更新时容易整体替换可读性差。界面上用 collectLatest 收集 loading 与 doctors加载中显示进度条加载失败显示重试按钮。两级页面共享同一个 ViewModel 时要注意用SavedStateHandle存 departmentId否则进程重建后找不到上下文。4.4 预约提交与进度条最小可用的 LoadingDialog提交预约的反馈要分三层提交中、提交成功、提交失败带原因号源被抢、重复预约、停诊不能只弹一个 Toast。我一般用一个带文案的 LoadingDialog 处理。class LoadingDialog(context: Context) : Dialog(context) { private val tvMessage: TextView ... // layout 里放一个 ProgressBar TextView fun showLoading(message: String) { tvMessage.text message setCancelable(false) show() } fun updateMessage(message: String) { tvMessage.text message } }setCancelable(false) 很关键否则用户点一下对话框外就会关掉提示以为系统没响应。成功或失败后记得 dismiss避免泄漏。进度条文案也要跟着状态走提交中是“正在锁定号源”成功后是“预约成功”失败时是“号源已被抢空请选择其他时段”。这些字符串要和状态机一一对应写进设计文档里。5. 本地缓存、登录态与安全细节不只在代码里加依赖5.1 Room 缓存最近浏览的医生与科室预约挂号首页的科室列表是热门入口接口很容易被打爆。常见做法是把科室列表缓存 12 小时下次启动先渲染缓存再请求增量更新。Room 的 DAO 实现简单直接。Dao interface DepartmentDao { Query(SELECT * FROM department WHERE is_hot 1) fun observeHot(): FlowListDepartmentEntity Insert(onConflict OnConflictStrategy.REPLACE) suspend fun insertAll(departments: ListDepartmentEntity) }在 Repository 里我一般会把“读数据库先返回 - 请求网络 - 对比更新时间决定是否写库”做成一个标准流程。注意 Room 的 Flow 和 StateFlow 不要混成两套订阅界面只收集一个来源。Room 的坑在于实体字段变更后必须升版本否则 Android 直接闪退。课程设计项目里最常遇到的问题就是“加了字段忘了 migration”所以字段变更时要同步改 build.gradle 里的 schema 版本并补 Migration。5.2 token 存储EncryptedSharedPreferences 而不是 SP用户的登录态和 token 如果直接放 SharedPreferences在 root 设备上等于裸奔。安全做法是使用 AndroidX Security 库的 EncryptedSharedPreferences它对键值做 AES 加密且密钥由设备级 Keystore 保护。val masterKey MasterKey.Builder(context) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build() val prefs EncryptedSharedPreferences.create( context, secure_prefs, masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM )写入 token、用户身份证号这类敏感信息的代码与普通 SharedPreferences 一样但要注意 EncryptedSharedPreferences 的读写性能比普通 SP 慢一个数量级所以只存 token 和 userId不要存业务列表。5.3 防刷与黄牛验证码、限流、设备指纹预约系统的黄牛问题集中在“脚本刷接口”和“身份信息代挂”。接口层最常见的做法是预约接口的前置验证码 同一设备指纹每分钟最多提交 3 次 同一个身份证号当天不能再预约。限流策略在服务端做Android 端只负责把设备指纹传过去不要在客户端写死阈值否则后端改策略又要发版。验证码方面预约高峰出现在早上 8 点到 9 点滑块验证码的依赖包如果加载过慢会直接拖垮入口。课程设计阶段用图形验证码就够重点是把“错误次数过多后锁定”这个分支写清楚。5.4 权限申请与通知兜底移动医疗场景里Android 权限申请要遵循最小化原则。预约挂号这个业务必须的权限只有网络和通知定位权限只在“按距离推荐医院”时才有必要。很多项目一上来就申请通讯录和存储权限这在应用商店审核时会直接被拒。通知权限要处理 Android 13 之后的 POST_NOTIFICATIONS 运行时申请。排队叫号推送如果拿不到权限至少要在 App 内做轮询兜底否则用户收不到叫号状态更新。6. 构建、测试与验收技巧最后一公里6.1 android studio 区分测试环境与正式环境用 buildConfigField 区分环境比手动改 baseUrl 可靠。buildTypes { debug { buildConfigField String, BASE_URL, \https://test.example.com/\ } release { buildConfigField String, BASE_URL, \https://api.example.com/\ } }打包测试包和正式包时不用改代码BuildConfig.BASE_URL会自动替换。模拟器和真机连测试环境时注意后端要开对应白名单否则请求直接失败。6.2 用 MockWebServer 在无后端时联调后端还在开发时接口联调依赖 MockWebServer 最直接。OkHttp 的 mockwebserver 依赖加进 testImplementation就可以在单元测试里返回固定 JSON。mockWebServer.enqueue( MockResponse().setBody({code:0,data:[]}) )把 Retrofit 的 baseUrl 指向mockWebServer.url(/)就能完整验证 ApiService 的反序列化和错误分支。“号源已抢空”这种错误响应也建议先 Mock 出来避免只在正式环境里第一次见到错误码。6.3 用 Profiler 和 Monkey 做体检上线前在 Android Studio 的 Profiler 里做一次内存和网络检查然后跑一轮 Monkey 压力测试。adb shell monkey -p com.example.hospital --throttle 200 --monitor-native-crashes 5000针对预约系统Monkey 重点测三个页面科室列表、医生列表、预约确认页连续点击后看是否崩溃、是否产生重复订单。重复订单不一定来自网络问题更多是按钮在 loading 期间仍可点击所以提交时要禁用按钮。6.4 设计文档的检查点毕业设计评审时答辩老师看重的不是代码量而是系统设计。对照自查是否给出用例图并覆盖停诊与超时支付分支是否给出 ER 图并标明排班与号源的 1:N 关系是否画出时序图说明预约提交的接口调用链是否在数据字典里写清楚 status 字段的所有取值。前四项齐全评审基本不会卡壳。在并发控制一节里直接贴第 2 章的乐观锁和条件更新说明能作为答辩亮点。本文还有配套的精品资源点击获取
返回列表