简介:本资源是一套面向计算机专业本科生的毕业设计级智慧医疗应用实战案例,聚焦医院预约挂号核心业务,适用于Android移动开发课程设计、毕设选题与健康医疗类App开发能力提升。项目采用Android Studio原生开发,包含完整客户端与轻量服务器端,涵盖患者注册登录、流行病学调查表填写、核酸/疫苗/门诊三级预约及记录查询、医保卡与就诊卡全生命周期管理等功能模块。压缩包共115个文件,含40个XML布局与配置文件、30张JPG/PNG界面素材、16个Java核心逻辑类、3个Gradle构建脚本及项目报告.docx等关键文档,整体4.87MB,结构清晰、模块解耦合理,便于理解MVC架构与SQLite本地数据管理实践。已有501人学习下载,提供可直接编译运行的源码、完整功能流程与规范文档,是掌握医疗类App开发全流程的优质参考范例。
1. 项目缘起与核心价值:为什么选择“智慧医疗预约挂号”作为毕业设计?
又到了一年一度的毕业季,相信不少计算机、软件工程专业的同学正在为毕业设计选题发愁。选个太简单的,怕工作量不够,答辩时被老师问住;选个太前沿的,又怕技术栈太新,资料难找,最后做不出来。如果你正面临这个困境,那么“基于Android Studio的智慧医疗医院预约挂号App”这个选题,或许是一个兼顾了技术深度、实用价值和实现可行性的“黄金选择”。
我当年毕业设计做的就是类似方向,后来在移动开发领域摸爬滚打多年,回头再看,这个选题确实能很好地锻炼一个准工程师的全栈思维。它不像一个单纯的“猜拳游戏”或“计算器”那样单薄,也不像“网约车平台”或“大型电商”那样庞大到难以驾驭。它恰好卡在一个“跳一跳能够得着”的难度上:你需要设计客户端与服务器端的交互,需要考虑数据的安全与存储,要处理复杂的业务逻辑(如科室选择、医生排班、号源锁定与释放),还要兼顾用户体验。完成这样一个项目,足以让你在简历上留下扎实的一笔,向面试官证明你具备独立开发一个完整移动应用的能力。
这个项目的核心价值在于,它模拟了一个真实世界中具有普遍需求的业务场景。通过它,你不仅能学会Android客户端的UI绘制、事件处理、网络请求(如使用Retrofit+OkHttp)、本地数据缓存(如Room数据库)等技能,更能深入到服务器端,理解RESTful API设计、数据库(如MySQL)表结构规划、业务逻辑层编写以及如何部署一个简单的后端服务(可以使用Spring Boot、Koa或Flask等轻量级框架)。这正是一个“全栈”概念的微型实践。
2. 技术架构全景:客户端与服务器端如何协同工作?
一个完整的智慧医疗预约挂号App,绝非一个孤立的手机程序。它背后是一套典型的C/S(客户端/服务器)架构在支撑。理解这套架构,是进行任何具体编码前最重要的一步。我们可以把它想象成去餐厅吃饭:客户端(App)是顾客手里的菜单和点餐平板,服务器端是后厨和收银系统。
2.1 客户端(Android App)的核心职责
客户端是用户直接交互的界面,它的主要任务可以概括为“展示、收集、发送、缓存”。
- 展示信息:以清晰、友好的界面展示医院列表、科室详情、医生简介、可预约时段(号源)、个人订单等信息。这里涉及到大量的UI组件使用,如RecyclerView展示列表、ViewPager2实现轮播图、复杂的表单布局等。
- 收集用户输入:处理用户的点击、选择、输入等操作。例如,选择就诊人、选择科室和医生、选择预约时间。
- 与服务器通信:将用户请求(如查询号源、提交预约)封装成HTTP请求,发送给服务器端API;并接收、解析服务器返回的JSON数据,更新UI。这是网络层的核心,通常我们会使用Retrofit库来简化这一过程。
- 本地数据缓存与状态管理:为了提升体验和实现部分离线功能,需要在本地缓存一些数据,如用户登录状态(使用SharedPreferences)、常用就诊人信息、医院科室的缓存数据(可以使用Room数据库)。这里的一个关键点是管理好本地与服务器数据的一致性。
2.2 服务器端(Server)的核心职责
服务器端是业务逻辑的大脑和数据中心,它必须做到“验证、处理、存储、响应”。
- 用户认证与授权:处理用户的注册、登录请求,验证用户名密码,生成并管理身份令牌(如JWT)。确保“预约”这类敏感操作只能是登录用户发起。
- 业务逻辑处理:这是最复杂的部分。例如:
- 号源管理:如何根据医生的排班计划,动态生成未来一段时间(如一周)的可预约时段?每个时段有多少个号?
- 预约逻辑:处理用户的预约请求时,需要检查号源是否充足、用户是否重复预约、同一时间段是否冲突。这是一个典型的“检查并设置”的原子操作,在高并发场景下(虽然毕业设计一般不考虑极高并发)需要特别注意,避免出现“超卖”(一个号被两个人预约)。简单的实现可以通过数据库的事务和行级锁来保证。
- 订单状态流转:预约成功后生成订单,订单可能还有“待支付”、“已预约”、“已取消”、“已完成”等状态,服务器需要驱动这些状态的变更。
- 数据持久化:将用户信息、医生信息、排班计划、预约订单等数据安全地存储到数据库中。需要精心设计数据表结构,建立合理的索引以优化查询性能。
- 提供API接口:定义一套清晰、规范的RESTful API供客户端调用。例如:
GET /api/hospitals获取医院列表GET /api/departments?hospital_id=1获取某医院科室列表GET /api/doctors/schedule?doctor_id=101&date=2023-10-27获取医生某天的排班POST /api/appointments提交预约订单GET /api/user/appointments获取当前用户的预约记录
2.3 数据交互流程(以预约为例)
让我们跟踪一次完整的预约操作,看看两端如何配合:
- 客户端:用户打开App,选择医院、科室,点击一位医生。App向服务器发送请求:
GET /api/doctors/schedule?doctor_id=xxx&start_date=xxx&end_date=xxx。 - 服务器端:接收到请求,查询数据库中的医生排班表和号源生成规则,计算出未来几天该医生每天的可用时间段和剩余号量,封装成JSON数组返回。
- 客户端:收到数据,以日历或列表形式展示可预约日期和时间段。用户选择某个时间段,点击“预约”。
- 客户端:App会检查用户是否已登录(检查本地Token),若未登录则跳转登录。登录后,将用户选择的时段信息、就诊人ID等,封装成一个JSON对象,通过
POST /api/appointments请求发送给服务器。这里务必在请求头中带上用户的身份认证Token。 - 服务器端:
- 鉴权:首先验证Token的有效性,确认是哪个用户发起的请求。
- 业务校验:检查该时段号源是否仍有余量(防止在用户点击后、请求到达前号被他人抢走)。
- 创建订单:在数据库事务中,执行“号源余量减1”和“创建一条预约订单记录”两个操作。只有两个操作都成功,事务才提交,否则回滚。这保证了数据的一致性。
- 响应:如果成功,返回预约成功的消息和订单号;如果失败(如号已约满),返回明确的错误原因。
- 客户端:根据服务器响应,显示“预约成功”的提示,并跳转到订单详情页;或显示失败提示,让用户重新选择。
注意:这个流程中,号源的“锁定”机制是关键。在真实的高并发场景下,可能会使用更复杂的分布式锁或消息队列来应对秒杀场景。但对于毕业设计,基于数据库事务的实现已经足够严谨,并能很好地体现你对核心业务逻辑和数据一致性的理解。
3. 客户端开发实战:从零搭建Android预约挂号App
有了架构蓝图,我们开始动手。客户端开发我们选择Android Studio,这是谷歌官方的IDE,对Android开发的支持最完善。假设你已经完成了Android Studio的环境搭建(包括SDK、模拟器配置等,如果启动不了模拟器,可以尝试使用真机调试或第三方模拟器如MuMu,并通过adb connect命令连接)。
3.1 项目结构与核心依赖
一个清晰的项目结构是良好开发的开端。推荐采用单Activity多Fragment的架构,结合Jetpack组件,这是目前Android官方推荐的主流架构模式。
com.wise.medical ├── data/ # 数据层 │ ├── local/ # 本地数据源 (Room, SharedPreferences) │ ├── remote/ # 远程数据源 (Retrofit接口定义) │ └── repository/ # 仓库,统一数据入口 ├── domain/ # 领域层(可选,存放业务模型和用例) ├── ui/ # 界面层 │ ├── hospital/ # 医院相关Fragment/ViewModel │ ├── appointment/# 预约相关Fragment/ViewModel │ └── user/ # 用户相关Fragment/ViewModel └── common/ # 通用组件、工具类、扩展函数在app/build.gradle中,你需要引入以下核心依赖:
dependencies { // 核心Jetpack组件 implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0' implementation 'androidx.fragment:fragment-ktx:1.6.2' // UI组件 implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.11.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'androidx.viewpager2:viewpager2:1.0.0' // 网络请求与序列化 implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0' // 本地数据库 implementation 'androidx.room:room-runtime:2.6.1' implementation 'androidx.room:room-ktx:2.6.1' kapt 'androidx.room:room-compiler:2.6.1' // 图片加载 implementation 'com.github.bumptech.glide:glide:4.16.0' kapt 'com.github.bumptech.glide:compiler:4.16.0' // 其他工具 implementation 'com.blankj:utilcodex:1.31.1' // 强大的工具类库 }3.2 网络层封装:用Retrofit优雅地调用API
网络层是客户端与服务器对话的桥梁。Retrofit是目前最主流的HTTP客户端库。封装的核心在于创建一个可全局访问、配置统一的Retrofit实例。
首先,定义一个API接口,描述所有服务器端点:
interface ApiService { @GET("hospitals") suspend fun getHospitals(): Response<BaseResponse<List<Hospital>>> @GET("departments") suspend fun getDepartments(@Query("hospitalId") hospitalId: Int): Response<BaseResponse<List<Department>>> @POST("auth/login") @FormUrlEncoded suspend fun login( @Field("username") username: String, @Field("password") password: String ): Response<BaseResponse<LoginResponse>> @GET("user/appointments") suspend fun getMyAppointments(@Header("Authorization") token: String): Response<BaseResponse<List<Appointment>>> @POST("appointments") suspend fun createAppointment( @Header("Authorization") token: String, @Body request: CreateAppointmentRequest ): Response<BaseResponse<Appointment>> }然后,创建一个单例的RetrofitClient对象:
object RetrofitClient { private const val BASE_URL = "http://your_server_ip:port/api/" // 替换为你的服务器地址 private val okHttpClient = OkHttpClient.Builder() .addInterceptor(LoggingInterceptor()) // 添加日志拦截器,方便调试 .addInterceptor { chain -> // 全局添加Token的拦截器 val originalRequest = chain.request() val token = UserManager.getToken() // 从本地获取Token val requestBuilder = originalRequest.newBuilder() if (token.isNotEmpty()) { requestBuilder.header("Authorization", "Bearer $token") } chain.proceed(requestBuilder.build()) } .connectTimeout(15, TimeUnit.SECONDS) .readTimeout(15, TimeUnit.SECONDS) .build() val apiService: ApiService by lazy { Retrofit.Builder() .baseUrl(BASE_URL) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create()) .build() .create(ApiService::class.java) } }实操心得:在调试阶段,务必添加
HttpLoggingInterceptor,它能在Logcat中打印出所有请求和响应的详细信息(URL、Header、Body),是排查网络问题的神器。但在发布版本前记得移除或关闭它。另外,全局Token拦截器的设计,避免了在每个API调用处手动添加Header的繁琐和遗漏。
3.3 关键页面实现:以预约流程为例
预约流程通常会涉及多个Fragment的跳转和数据传递。我们可以使用Android Jetpack的Navigation组件来管理。
页面流:HospitalListFragment->DepartmentListFragment->DoctorListFragment->ScheduleFragment->ConfirmAppointmentFragment
数据传递:在Fragment之间传递复杂对象(如医院、医生)时,强烈建议使用ViewModel结合SharedViewModel或Safe Args,而不是直接通过Bundle传递。因为Bundle有大小限制,且传递自定义对象需要序列化,比较麻烦。
例如,我们可以创建一个AppointmentSharedViewModel,它继承自ViewModel,并作用于整个Activity(通过by activityViewModels()获取)。
class AppointmentSharedViewModel : ViewModel() { // 使用LiveData来持有当前选中的医院、科室、医生等信息 private val _selectedHospital = MutableLiveData<Hospital?>() val selectedHospital: LiveData<Hospital?> = _selectedHospital fun selectHospital(hospital: Hospital) { _selectedHospital.value = hospital } // ... 类似地定义 selectedDepartment, selectedDoctor, selectedSchedule }在HospitalListFragment中,当用户点击一个医院时:
viewModel.selectHospital(clickedHospital) // 然后通过Navigation跳转到DepartmentListFragment findNavController().navigate(R.id.action_hospitalList_to_departmentList)在DepartmentListFragment中,直接获取共享的ViewModel并观察选中的医院:
private val sharedViewModel: AppointmentSharedViewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedViewModel.selectedHospital.observe(viewLifecycleOwner) { hospital -> hospital?.let { // 根据选中的医院ID,去加载对应的科室列表 loadDepartments(it.id) } } }这种方式保证了数据在流程中的一致性,且避免了在复杂的页面跳转中丢失状态。
ScheduleFragment(号源选择页)的实现细节: 这个页面通常是日历或时间列表。使用RecyclerView展示未来几天的日期,点击日期后,下方另一个RecyclerView展示该日期下的具体时间段。每个时间段Item需要显示“时间点”和“剩余号数”。当号数为0时,Item应变为不可点击状态并置灰。
这里的关键是数据绑定与状态更新。你的Adapter数据源应该是一个List<TimeSlot>对象列表。当用户点击一个时间段时,除了在UI上高亮显示,更重要的是要立即向服务器发送一个“锁定号源”的预请求吗?不,通常不这样做。在最终确认预约前,号源只是“显示”剩余量。真正的锁定发生在提交整个预约订单的API调用中,由服务器端的事务来保证原子性。客户端能做的,是在用户点击“确认预约”按钮后,立即禁用按钮并显示加载中,防止用户重复提交,直到收到服务器的成功或失败响应。
4. 服务器端开发精要:构建稳健的后台API服务
客户端光鲜亮丽的背后,需要一个稳定可靠的服务器。对于毕业设计,我们不必追求微服务、分布式等复杂架构,一个结构清晰的单体应用足矣。这里以Spring Boot (Java) 为例,因为它生态成熟,资料丰富,非常适合快速开发。
4.1 技术选型与项目结构
- 框架:Spring Boot 2.x
- 数据库:MySQL 8.0 (关系型数据库最适合这种结构化数据)
- ORM:MyBatis-Plus (比JPA更灵活,SQL可控性强)
- 身份认证:JWT (JSON Web Token)
- 项目管理:Maven
项目结构建议如下:
src/main/java/com/wise/medical/server ├── MedicalAppServerApplication.java // 启动类 ├── config/ // 配置类(Web, Mybatis, 拦截器等) ├── controller/ // 控制器层,接收HTTP请求 ├── service/ // 业务逻辑层 │ └── impl/ // 业务逻辑实现类 ├── mapper/ // MyBatis Mapper接口层 ├── entity/ // 实体类,对应数据库表 ├── dto/ // 数据传输对象(请求/响应) ├── common/ // 通用类(统一响应体、异常、工具类) └── interceptor/ // 拦截器(如JWT认证拦截器)4.2 数据库设计核心表结构
数据库设计是项目的基石。以下是几个核心表的设计思路:
1. 用户表 (user)
CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(255) NOT NULL COMMENT '加密后的密码', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `id_card` varchar(20) DEFAULT NULL COMMENT '身份证号', `avatar` varchar(500) DEFAULT NULL COMMENT '头像URL', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) COMMENT='用户表';注意:密码字段务必使用强哈希算法(如BCrypt)加密存储,绝对禁止明文保存。
2. 医院与科室表 (hospital, department)这是典型的树形结构。一个医院有多个科室。
CREATE TABLE `hospital` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '医院名称', `level` varchar(20) DEFAULT NULL COMMENT '医院等级', `address` varchar(500) DEFAULT NULL COMMENT '地址', `intro` text COMMENT '简介', PRIMARY KEY (`id`) ) COMMENT='医院表'; CREATE TABLE `department` ( `id` int NOT NULL AUTO_INCREMENT, `hospital_id` int NOT NULL COMMENT '所属医院ID', `name` varchar(100) NOT NULL COMMENT '科室名称', `intro` text COMMENT '科室介绍', PRIMARY KEY (`id`), KEY `idx_hospital_id` (`hospital_id`), CONSTRAINT `fk_dept_hospital` FOREIGN KEY (`hospital_id`) REFERENCES `hospital` (`id`) ) COMMENT='科室表';3. 医生与排班表 (doctor, schedule)这是业务的核心。一个医生属于一个科室。排班表定义了医生在特定日期的可预约情况。
CREATE TABLE `doctor` ( `id` int NOT NULL AUTO_INCREMENT, `department_id` int NOT NULL COMMENT '所属科室ID', `name` varchar(50) NOT NULL COMMENT '医生姓名', `title` varchar(50) DEFAULT NULL COMMENT '职称', `specialty` varchar(500) DEFAULT NULL COMMENT '擅长领域', `photo` varchar(500) DEFAULT NULL COMMENT '照片URL', PRIMARY KEY (`id`), KEY `idx_department_id` (`department_id`), CONSTRAINT `fk_doctor_dept` FOREIGN KEY (`department_id`) REFERENCES `department` (`id`) ) COMMENT='医生表'; CREATE TABLE `schedule` ( `id` int NOT NULL AUTO_INCREMENT, `doctor_id` int NOT NULL COMMENT '医生ID', `work_date` date NOT NULL COMMENT '排班日期', `time_slot` varchar(20) NOT NULL COMMENT '时间段,如 09:00-10:00', `total_quota` int NOT NULL DEFAULT 0 COMMENT '该时段总号源数', `available_quota` int NOT NULL DEFAULT 0 COMMENT '剩余可预约号源数', `is_valid` tinyint(1) DEFAULT 1 COMMENT '是否有效(医生可能临时停诊)', PRIMARY KEY (`id`), UNIQUE KEY `uk_doctor_time` (`doctor_id`,`work_date`,`time_slot`), -- 防止重复排班 KEY `idx_doctor_date` (`doctor_id`,`work_date`), CONSTRAINT `fk_schedule_doctor` FOREIGN KEY (`doctor_id`) REFERENCES `doctor` (`id`) ) COMMENT='医生排班与号源表';核心设计点:
schedule表将“排班计划”和“实时号源”合二为一。available_quota字段是关键,每次成功预约都需要原子性地将其减1。is_valid字段用于处理医生临时停诊等异常情况。
4. 预约订单表 (appointment)
CREATE TABLE `appointment` ( `id` varchar(32) NOT NULL COMMENT '订单号,可使用雪花算法生成', `user_id` int NOT NULL COMMENT '用户ID', `schedule_id` int NOT NULL COMMENT '预约的排班ID', `patient_name` varchar(50) NOT NULL COMMENT '就诊人姓名', `patient_id_card` varchar(20) DEFAULT NULL COMMENT '就诊人身份证', `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:1待支付,2已预约,3已取消,4已完成', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `pay_time` datetime DEFAULT NULL COMMENT '支付时间', `cancel_time` datetime DEFAULT NULL COMMENT '取消时间', PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_schedule_id` (`schedule_id`), CONSTRAINT `fk_appointment_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`), CONSTRAINT `fk_appointment_schedule` FOREIGN KEY (`schedule_id`) REFERENCES `schedule` (`id`) ) COMMENT='预约订单表';4.3 核心业务逻辑实现:预约与号源扣减
这是整个服务器端最需要严谨处理的逻辑。我们必须在Service层的一个方法中,完成“检查号源”和“创建订单”这两个操作,并且要保证其原子性。
@Service @Transactional(rollbackFor = Exception.class) // 声明式事务,发生异常自动回滚 public class AppointmentServiceImpl implements AppointmentService { @Autowired private ScheduleMapper scheduleMapper; @Autowired private AppointmentMapper appointmentMapper; @Override public String createAppointment(CreateAppointmentDTO dto, Integer userId) { // 1. 查询排班信息,并锁定行(使用 for update) Schedule schedule = scheduleMapper.selectScheduleForUpdate(dto.getScheduleId()); if (schedule == null) { throw new BusinessException("排班信息不存在"); } if (schedule.getAvailableQuota() <= 0) { throw new BusinessException("该时段号源已约满"); } if (!schedule.getIsValid()) { throw new BusinessException("该时段已停诊"); } // 2. 检查用户是否已有同一时间段的预约(业务规则) // ... 省略查询逻辑 // 3. 扣减号源 int updateCount = scheduleMapper.decreaseQuota(dto.getScheduleId()); if (updateCount == 0) { // 理论上不会走到这里,因为前面已经判断并锁定了。但为安全起见。 throw new ConcurrentBookingException("号源并发变更,预约失败"); } // 4. 生成订单记录 Appointment appointment = new Appointment(); // 使用雪花算法生成分布式ID appointment.setId(IdGenerator.generateId()); appointment.setUserId(userId); appointment.setScheduleId(dto.getScheduleId()); appointment.setPatientName(dto.getPatientName()); appointment.setStatus(AppointmentStatus.PENDING_PAYMENT.getCode()); // 初始状态:待支付 appointmentMapper.insert(appointment); // 5. 返回订单号 return appointment.getId(); } }对应的Mapper SQL需要支持行锁:
<!-- ScheduleMapper.xml --> <select id="selectScheduleForUpdate" resultType="Schedule"> SELECT * FROM schedule WHERE id = #{id} FOR UPDATE </select> <update id="decreaseQuota"> UPDATE schedule SET available_quota = available_quota - 1 WHERE id = #{id} AND available_quota > 0 </update>踩坑实录:这里最大的坑就是并发控制。如果不用
SELECT ... FOR UPDATE进行悲观锁,或者在decreaseQuota的WHERE条件中不检查available_quota > 0,在高并发下就可能出现“超卖”。FOR UPDATE会在事务中锁定这行数据,直到事务提交,其他想修改这行数据的事务必须等待。这虽然会降低一点并发性能,但对于毕业设计级别的项目,是简单且可靠的选择。务必在@Transactional注解的方法中执行整个流程,确保“查询-判断-更新”是一个原子操作。
4.4 用户认证与API安全:使用JWT
RESTful API是无状态的,我们需要一种机制来识别用户。JWT是一种流行的方案。
- 用户登录:用户提交用户名密码,服务器验证成功后,使用密钥(如HMAC SHA256)生成一个JWT Token,返回给客户端。
- Token携带:客户端后续请求,在HTTP Header的
Authorization字段中携带此Token(格式:Bearer <token>)。 - Token验证:服务器编写一个拦截器(Interceptor),对需要认证的API路径进行拦截,解析并验证Token的有效性(是否过期、签名是否正确),并从Token中提取用户信息(如userId),存入当前请求上下文。
JWT工具类示例:
@Component public class JwtUtil { @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private Long expiration; public String generateToken(String username, Integer userId) { Date now = new Date(); Date expiryDate = new Date(now.getTime() + expiration); return Jwts.builder() .setSubject(username) .claim("userId", userId) // 自定义声明 .setIssuedAt(now) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } public Claims getClaimsFromToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } // ... 其他验证方法 }认证拦截器示例:
public class AuthenticationInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 1. 从Header中获取Token String authHeader = request.getHeader("Authorization"); if (authHeader == null || !authHeader.startsWith("Bearer ")) { throw new UnauthorizedException("缺少有效的认证信息"); } String token = authHeader.substring(7); // 2. 验证并解析Token Claims claims = jwtUtil.getClaimsFromToken(token); Integer userId = claims.get("userId", Integer.class); // 3. 将用户信息存入请求上下文(如ThreadLocal) UserContext.setCurrentUserId(userId); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 请求结束后,清除上下文,防止内存泄漏 UserContext.clear(); } }5. 项目部署与联调:让App真正跑起来
开发完成后的联调和部署是临门一脚。很多同学的项目在本地跑得好好的,一到真机或服务器就各种问题。
5.1 服务器端部署(简易版)
对于毕业设计,你可以选择以下一种方式部署你的Spring Boot后端:
- 本地运行:直接在IDE里运行,手机和电脑在同一局域网下,将客户端代码中的
BASE_URL改为你的电脑IP(如http://192.168.1.100:8080/api/)。这是最简单的调试方式。 - 云服务器部署:购买一台最基础的云服务器(如腾讯云、阿里云的轻量应用服务器),安装JDK和MySQL。将你的项目打成JAR包,使用
scp命令上传到服务器,然后用nohup java -jar your-app.jar &命令后台运行。记得在云服务器的安全组(防火墙)中开放你应用使用的端口(如8080)。 - 容器化部署(进阶):使用Docker将你的应用和MySQL数据库容器化,通过
docker-compose.yml一键启动。这更接近现代部署方式,可以在简历中加分。
避坑指南:部署到服务器后,最常见的两个问题是数据库连接失败和跨域问题(CORS)。
- 数据库:确保服务器上的MySQL服务已启动,并且允许从你的应用所在IP进行连接(可能需要修改MySQL的
bind-address或用户权限)。- 跨域:因为你的Android App(客户端)和Spring Boot服务(服务器)域名/端口不同,浏览器(或WebView)会因同源策略阻止请求。必须在Spring Boot后端配置CORS。可以创建一个全局的
WebMvcConfigurer:@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") // 针对所有/api/开头的接口 .allowedOrigins("*") // 允许所有来源,生产环境应指定具体域名 .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(false); // 如果前端需要传cookie,设为true } }
5.2 客户端真机调试与打包
- 真机调试:用USB线连接安卓手机,打开开发者选项和USB调试。在Android Studio中运行项目时,选择你的真机设备。这比模拟器更真实,能测试网络、权限等。
- 修改网络配置:确保手机和运行服务器的电脑/云服务器在同一个网络下,或者手机能访问到云服务器的公网IP。在
RetrofitClient中正确配置BASE_URL。 - 打包APK:在Android Studio中,选择
Build->Generate Signed Bundle / APK。你需要创建一个密钥库(Keystore)来签名你的应用。务必保管好这个密钥库文件和密码,这是你应用的唯一身份标识,未来上架应用市场或更新版本都需要它。 - 权限处理:如果你的App需要网络、存储等权限,记得在
AndroidManifest.xml中声明,并在运行时(针对Android 6.0+)动态申请。
5.3 文档编写与答辩准备
毕业设计除了代码,文档和答辩同样重要。
项目文档应至少包含:
- 需求分析:阐述项目背景、解决的问题、用户角色、功能模块(如用户管理、医院科室浏览、预约挂号、订单管理)。
- 系统设计:包括架构图(C/S)、功能模块图、数据库ER图、核心API接口文档(可以使用Swagger集成来自动生成)。
- 核心代码说明:挑选2-3个最核心的流程(如登录认证、预约下单),用流程图或序列图说明,并附上关键代码片段和注释。
- 部署说明:详细列出运行本项目所需的环境(JDK版本、MySQL版本、Android SDK版本)和步骤。
- 测试报告:描述你进行了哪些测试(功能测试、界面测试、网络异常测试),并附上测试用例和结果截图。
答辩准备要点:
- 演示流程要流畅:准备一个从打开App、登录、浏览、预约到查看订单的完整流程。确保网络通畅,服务器稳定。
- 重点讲清楚技术难点和解决方案:不要平铺直叙地介绍功能。重点阐述你是如何解决“号源并发”、“用户认证”、“数据一致性”这些技术挑战的。这能体现你的思考深度。
- 准备好回答“为什么”:老师可能会问“为什么用Retrofit不用Volley?”、“为什么用JWT不用Session?”、“你的数据库表为什么这样设计?”。对你的技术选型要有充分的理由。
- 展示你的文档和代码:代码结构清晰、有注释,能为你加分不少。
最后,这个项目虽然是一个毕业设计,但如果你能按照上述思路,扎扎实实地完成客户端和服务器端的每一个环节,并深入思考其中的技术细节,那么它绝对能成为你求职路上一个非常有力的作品。它不仅证明了你的编码能力,更证明了你的系统思维和解决实际问题的能力。祝你毕业设计顺利,答辩成功!
本文还有配套的精品资源,点击获取