简介:这是一套面向高校安卓课程设计与移动应用开发学习者的完整项目资料,源自大三学期课程作业,由两人协作约两个月完成,涵盖Android客户端、后端数据接口与简易Web管理后台三部分,适合作为课程设计参考、毕业设计雏形或参赛作品的起步模板。压缩包共1106个文件,约74.32MB,以json、xml、png、java、class、dex、jar、properties、html、css等为主,分别对应接口数据、界面布局、图标素材、业务源码、编译产物与前端页面,结构完整便于对照学习。目前已有1017人学习下载。资料包含Android端源码、后端源码、Web管理系统源码,以及一份按论文格式撰写、约13000字共60页的课程报告,可帮助读者理解信息化医疗服务系统的接口设计思路、App与后台的协作方式,并借鉴报告撰写与项目组织经验。
1. 从一份 Android 医疗系统源码包说起:它到底能跑出什么
如果你手头正好有一份「基于 Android 的信息化医疗服务系统」的压缩包,第一反应大概率是:这玩意儿是课程设计凑数的,还是真能拆出点东西?我拿到这类包的习惯是先看技术栈——Android 客户端 + SpringBoot 后端 + jFinal 框架,这个组合在近两年的毕设和中小型医疗信息化项目里出现频率极高。它解决的核心问题很具体:让患者端能挂号、查报告、看排班,让医护端能录入诊断、管理病历,后台用 REST 接口把两端串起来。适合谁?正在做医疗类 App 课程设计的学生、需要快速搭一个院内服务原型的小团队、以及想研究 Android 与 Java 后端如何做数据同步的开发者。别指望它直接上三甲医院的生产环境,但作为一套能跑通「注册登录→科室选择→预约挂号→报告查询」闭环的参考实现,它的骨架是完整的。下面我按实际拆包和部署的顺序,把这份资源从环境配置到接口联调再到避坑,一层层剥开。
2. 环境搭建与工程结构:把 Android 端和后端同时跑起来
2.1 开发环境版本对齐
这类包最常见的翻车点不是代码逻辑,而是版本对不上。Android 端通常用 Android Studio 打开,Gradle 版本和 AGP 版本必须匹配;后端用 IDEA 或 Eclipse 导入 Maven 工程,JDK 版本要跟 SpringBoot 的依赖兼容。我一般会先看三个文件:build.gradle(项目级)、app/build.gradle(模块级)、pom.xml。如果 Gradle 插件写的是 7.x,那 Android Studio 至少要是 Arctic Fox 以上;如果 SpringBoot 是 2.7.x,JDK 选 8 或 11 最稳,别上来就 JDK 17,jFinal 的一些老依赖在模块化系统下会报InaccessibleObjectException。
# 查看本机 JDK 版本,确认与 pom.xml 中 java.version 一致 java -version # 输出示例:openjdk version "11.0.20" 2023-08-17 # 查看 Gradle 版本(在 Android 工程根目录下执行) ./gradlew --version # 重点看 Gradle 和 AGP 的对应关系,不匹配就先改 gradle-wrapper.properties上面两条命令是动手前必须走的。java -version决定后端能不能编译,./gradlew --version决定 Android 端同步会不会卡在Downloading。参数上,gradle-wrapper.properties里的distributionUrl如果指向的是services.gradle.org的旧版本,国内网络下同步极慢,常见做法是换成腾讯云或阿里云的镜像地址,但注意镜像只加速下载,不改变版本号本身。
2.2 工程目录拆解与模块职责
解压后一般能看到两个顶层目录:一个是 Android 客户端工程(含app/src/main/java下的 Activity、Adapter、网络请求封装),另一个是后端服务工程(含src/main/java下的 Controller、Service、Dao)。jFinal 在这里通常出现在后端的数据访问层,它比 MyBatis 更轻,配置少,适合快速出活。你要关注的是AndroidManifest.xml里的权限声明和application.yml里的数据库连接。
<!-- AndroidManifest.xml 中必须有的网络权限和存储权限 --> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- 如果涉及报告图片缓存,还需要读写外部存储,Android 10+ 要用分区存储 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" />这段权限配置直接决定 App 能不能发出网络请求。INTERNET是底线,没有它所有接口调用都会抛SecurityException。ACCESS_NETWORK_STATE用于判断网络可用性,很多包里的工具类会用它做断网提示。注意READ_EXTERNAL_STORAGE加了maxSdkVersion="28",这是为了兼容 Android 10 以下,Android 10 及以上应该走 MediaStore 或应用专属目录,否则会触发分区存储限制。后端这边,application.yml里重点看spring.datasource.url、username、password,以及server.port。如果端口写的是 8080,而 Android 端Retrofit的baseUrl写的是10.0.2.2:8080,那模拟器才能通;真机调试必须换成电脑局域网 IP。
2.3 数据库初始化与接口连通性验证
后端跑起来之前,数据库得先建好。包里一般会带一个.sql文件,用 Navicat 或命令行导入 MySQL。导入后别急着启动 SpringBoot,先用mysql命令行确认表结构和数据。
-- 确认核心表是否创建成功,医疗系统一般至少这几张 SHOW TABLES; -- 预期看到:user、department、doctor、appointment、report 等 -- 检查科室表有没有初始数据,没有的话前端科室列表会是空的 SELECT * FROM department LIMIT 5; -- 检查用户表字段,确认密码字段长度够不够存加密后的串 DESC user;导入 SQL 后执行SHOW TABLES是最快的验证手段。如果表数量明显偏少,说明 SQL 文件不完整或者执行中断了。SELECT * FROM department用来确认基础数据,很多包只建表不插数据,导致 App 首页科室列表空白,这不是代码 bug,是数据没初始化。DESC user看密码字段,如果长度只有 20,而代码里用的是 BCrypt 加密(密文长度 60),插入时会被截断,登录必然失败。后端启动后,用浏览器或 Postman 访问http://localhost:8080/department/list,返回 JSON 数组就说明接口通了。Android 端这边,把baseUrl配好,运行到模拟器,注册一个账号,能收到验证码或直接登录成功,整条链路就算打通了。
3. 核心功能实现:挂号、报告查询与数据同步的代码路径
3.1 预约挂号模块的接口设计与参数传递
挂号是这类系统的核心业务,代码路径通常是从 Android 端的AppointmentActivity发起,经过Retrofit封装的ApiService,打到后端的AppointmentController,再经AppointmentService调用 jFinal 的Db工具类写库。这里的关键参数有四个:userId、doctorId、scheduleId、appointmentTime。少一个后端就不知道给谁排给谁看。
// Android 端 Retrofit 接口定义(简化版) public interface ApiService { @FormUrlEncoded @POST("appointment/create") Call<BaseResponse> createAppointment( @Field("userId") int userId, @Field("doctorId") int doctorId, @Field("scheduleId") int scheduleId, @Field("appointmentTime") String appointmentTime ); }这段代码里@FormUrlEncoded和@Field是配套的,表示以表单形式提交。userId一般从 SharedPreferences 里取登录后存的用户 ID,doctorId和scheduleId来自前一个页面的列表点击事件,appointmentTime是字符串格式的日期时间,后端用SimpleDateFormat解析。常见坑是时间格式不统一,Android 端传yyyy-MM-dd HH:mm,后端按yyyy/MM/dd解析,直接抛ParseException。我一般会在接口文档里把格式写死,两端都用同一个常量。
// 后端 Controller 接收参数并调用 Service @PostMapping("/appointment/create") public BaseResponse create(@RequestParam int userId, @RequestParam int doctorId, @RequestParam int scheduleId, @RequestParam String appointmentTime) { // 先查该医生该时段是否还有余号 int remaining = appointmentService.getRemaining(scheduleId); if (remaining <= 0) { return BaseResponse.fail("该时段号源已满"); } // 再查该用户是否重复预约 boolean duplicated = appointmentService.isDuplicated(userId, scheduleId); if (duplicated) { return BaseResponse.fail("您已预约过该时段"); } return appointmentService.create(userId, doctorId, scheduleId, appointmentTime); }后端这段逻辑有两个前置校验:余号检查和重复预约检查。getRemaining一般查schedule表的total减去appointment表里已预约数量。isDuplicated查同一用户同一排班是否已有记录。这两个校验不做,就会出现超卖和一人占多号的情况,在医疗场景里是业务事故。参数上,scheduleId是排班主键,不是医生 ID,别搞混。返回的BaseResponse通常包含code、msg、data三个字段,Android 端根据code判断成功失败并弹 Toast。
3.2 检查报告查询与列表渲染
报告查询模块的数据流是反过来的:后端从report表按userId查列表,Android 端用RecyclerView渲染。这里容易出问题的地方是图片路径和分页。报告如果有影像图片,数据库里存的往往是相对路径,Android 端需要拼上后端文件服务的域名或 IP 才能加载。
// Android 端 RecyclerView Adapter 的 onBindViewHolder 片段 @Override public void onBindViewHolder(ViewHolder holder, int position) { Report report = reportList.get(position); holder.tvTitle.setText(report.getReportName()); holder.tvDate.setText(report.getCreateTime()); // 图片路径拼接,BASE_URL 来自 BuildConfig 或常量类 String imgUrl = Constants.BASE_URL + report.getImagePath(); Glide.with(holder.itemView.getContext()) .load(imgUrl) .placeholder(R.drawable.ic_loading) .into(holder.ivReport); }Glide加载图片时,BASE_URL必须和后端文件映射路径一致。常见做法是后端用WebMvcConfigurer把/upload/**映射到本地磁盘目录,数据库存/upload/report/xxx.jpg,Android 端拼http://10.0.2.2:8080/upload/report/xxx.jpg。如果图片加载不出来,先看placeholder有没有显示,有占位图说明请求发出了但没拿到,大概率是路径拼错或后端静态资源映射没配。分页这块,如果报告列表数据多,后端要支持page和size参数,Android 端做上拉加载,别一次性全查出来,否则列表卡顿。
3.3 jFinal 在后端数据层的实际用法
jFinal 在这类项目里主要用来简化数据库操作。它的Db工具类可以直接执行 SQL 并返回Record或List<Record>,比手写 JDBC 省事,比 MyBatis 少配置。但要注意,jFinal 需要配置ActiveRecordPlugin和DruidPlugin,并且要在JFinalConfig里注册。
// jFinal 配置类片段 public class AppConfig extends JFinalConfig { @Override public void configPlugin(Plugins me) { // 数据库连接池 DruidPlugin dp = new DruidPlugin("jdbc:mysql://localhost:3306/medical", "root", "123456"); me.add(dp); // ActiveRecord 插件 ActiveRecordPlugin arp = new ActiveRecordPlugin(dp); arp.setShowSql(true); // 开发期打开,方便看 SQL me.add(arp); } }DruidPlugin负责连接池,ActiveRecordPlugin负责 ORM 映射。setShowSql(true)在开发阶段很有用,控制台会打印实际执行的 SQL,方便排查字段名写错的问题。参数上,数据库 URL 要加useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai,否则中文乱码和时间差 8 小时是常态。jFinal 的Db.find("select * from report where user_id = ?", userId)这种写法,占位符?按顺序传参,别用字符串拼接,会有 SQL 注入风险。如果查询报Table 'medical.report' doesn't exist,先确认数据库名和表名大小写,Linux 下 MySQL 默认区分大小写。
4. 避坑与排查:从编译失败到接口 404 的常见问题
4.1 Gradle 同步卡死或依赖下载失败
现象:打开 Android 工程后,Gradle 一直转圈,控制台停在Downloading https://services.gradle.org/...。原因:默认仓库地址在国内访问不稳定,或者 Gradle 版本与 AGP 不匹配。解决:改build.gradle里的仓库为阿里云镜像,同时确认gradle-wrapper.properties里的版本号与 AGP 对应。
// 项目级 build.gradle 中替换仓库地址 buildscript { repositories { maven { url 'https://maven.aliyun.com/repository/google' } maven { url 'https://maven.aliyun.com/repository/public' } maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } } }改完仓库后点Sync Now,如果还卡,删掉.gradle缓存目录重新同步。注意镜像只加速下载,不解决版本冲突,AGP 7.0 配 Gradle 7.0+,AGP 4.2 配 Gradle 6.7.1,这个对应关系不能乱。
4.2 后端启动报数据库连接失败
现象:SpringBoot 启动时抛Communications link failure或Access denied for user。原因:数据库没启动、端口不对、用户名密码错、或者 MySQL 8 的驱动类名还是旧的com.mysql.jdbc.Driver。解决:先telnet localhost 3306确认端口通,再检查application.yml里的账号密码,MySQL 8 要用com.mysql.cj.jdbc.Driver并加时区参数。
# application.yml 中 MySQL 8 的正确配置 spring: datasource: url: jdbc:mysql://localhost:3306/medical?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.DriveruseSSL=false在开发环境可以关掉,避免证书警告。serverTimezone不设的话,插入时间会差 8 小时,报告时间显示不对。
4.3 Android 端接口请求返回 404 或 500
现象:App 点登录没反应,Logcat 里看到HTTP 404或HTTP 500。原因:baseUrl写错、接口路径不匹配、后端没启动、或者参数缺失导致空指针。解决:先用浏览器访问后端接口确认返回正常,再检查 Android 端Retrofit的baseUrl是否以/结尾,接口注解里的路径是否多了或少了前缀。
// Retrofit 构建时 baseUrl 必须以 / 结尾 Retrofit retrofit = new Retrofit.Builder() .baseUrl("http://10.0.2.2:8080/") // 注意末尾斜杠 .addConverterFactory(GsonConverterFactory.create()) .build();baseUrl末尾没斜杠,拼接时会丢掉路径段。模拟器用10.0.2.2访问宿主机,真机要用电脑的局域网 IP,且手机和电脑在同一 WiFi 下。500 错误看后端控制台堆栈,通常是空指针或 SQL 异常。
4.4 真机调试时网络请求被拦截
现象:模拟器能跑,真机安装后所有接口超时。原因:Android 9 及以上默认禁止明文 HTTP 请求,而这类老包通常用的还是http://。解决:在AndroidManifest.xml的application标签加android:usesCleartextTraffic="true",或者配network_security_config白名单。
<application android:usesCleartextTraffic="true" ...> </application>这个属性在开发期最省事,但上线前要改成 HTTPS 或配白名单,否则应用商店审核不过。另外检查手机是否开了省电模式,部分机型会限制后台网络。
4.5 报告图片加载不出来
现象:报告列表有文字,但图片位置一直是占位图。原因:图片路径拼接错误、后端静态资源映射没配、或者 Android 10+ 分区存储导致本地缓存失败。解决:先在浏览器直接访问图片 URL,能打开说明后端没问题,问题在 Android 端拼接;打不开就检查后端WebMvcConfigurer的addResourceHandlers有没有映射/upload/**。
// 后端静态资源映射配置 @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + System.getProperty("user.dir") + "/upload/"); }file:后面跟的是绝对路径,user.dir是项目启动目录,确保upload文件夹真实存在且有图片文件。路径拼错是最高频的原因,我一般会在 Android 端把完整 URL 打日志出来,一眼就能看出问题。
5. 进阶技巧:用接口幂等和本地缓存把系统做稳
5.1 挂号接口的幂等设计
医疗系统里最怕重复挂号。用户手抖点两下,或者网络超时后重试,都可能生成两条预约记录。常见做法是在后端加幂等控制:用userId + scheduleId做唯一索引,插入时捕获DuplicateKeyException,返回「请勿重复提交」。
-- 在 appointment 表上建唯一索引 ALTER TABLE appointment ADD UNIQUE KEY uk_user_schedule (user_id, schedule_id);// Service 层捕获唯一键冲突 try { Db.update("insert into appointment(user_id, doctor_id, schedule_id, appointment_time) values(?,?,?,?)", userId, doctorId, scheduleId, appointmentTime); return BaseResponse.success("预约成功"); } catch (DuplicateKeyException e) { return BaseResponse.fail("您已预约过该时段,请勿重复提交"); }唯一索引是最后一道防线,比先查后插更可靠,因为查和插之间有并发窗口。DuplicateKeyException是 Spring 对 SQL 唯一键冲突的封装,捕获后返回友好提示。这个改动很小,但能避免大部分重复预约的脏数据。
5.2 Android 端本地缓存与离线展示
患者查报告时,如果网络不好,每次都要等接口返回,体验很差。我一般会在 Android 端加一层本地缓存:用 Room 或 SQLite 存最近一次的报告列表,进入页面先读缓存展示,再发请求更新。
// 用 Room 做报告列表缓存(简化示例) @Dao public interface ReportDao { @Query("SELECT * FROM report WHERE userId = :userId ORDER BY createTime DESC") List<Report> getByUserId(int userId); @Insert(onConflict = OnConflictStrategy.REPLACE) void insertAll(List<Report> reports); }OnConflictStrategy.REPLACE保证同一条报告重复插入时覆盖而不是报错。进入页面时先getByUserId读本地,再调接口,接口返回后insertAll更新缓存并刷新 UI。这样即使断网,用户也能看到上次的报告列表。注意缓存要有过期策略,比如超过 24 小时强制走网络,避免展示过时数据。
5.3 用日志和断点定位接口参数错位
接口联调时最头疼的是参数对不上。我的习惯是在 Android 端加HttpLoggingInterceptor,把请求 URL、请求体、响应体全打出来;后端在 Controller 入口打一行日志,记录接收到的参数。
// Android 端 OkHttp 日志拦截器 HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); logging.setLevel(HttpLoggingInterceptor.Level.BODY); OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(logging) .build();// 后端 Controller 入口日志 @PostMapping("/appointment/create") public BaseResponse create(@RequestParam int userId, @RequestParam int doctorId, @RequestParam int scheduleId, @RequestParam String appointmentTime) { log.info("create appointment: userId={}, doctorId={}, scheduleId={}, time={}", userId, doctorId, scheduleId, appointmentTime); // ... 后续逻辑 }两边日志一对,就能看出是 Android 端传错了,还是后端接错了。HttpLoggingInterceptor.Level.BODY会打印请求体,但注意别在正式环境开,会泄露敏感信息。后端日志用log.info而不是System.out.println,方便按级别过滤。从那以后我每次联调新接口,都强制先看两边日志再写业务逻辑,省下的排查时间比写日志的时间多得多。希望帮到你。
本文还有配套的精品资源,点击获取