简介:这是一份面向高校计算机专业学生与Java初学者的毕业设计完整源码包,围绕老年人服药提醒App展开,帮助解决按时按量服药管理这一实际需求。项目以Java 1.8为基础,结合Android四大组件、MySQL 5.7+数据库与IntelliJ IDEA或Eclipse开发环境,涵盖用户信息、药物信息与服药计划等模块,并附有数据库脚本与环境部署说明,便于快速搭建运行环境。压缩包共299个文件,约48.17MB,包含56个java源码、58个class、86个jar依赖、24个vue前端文件,以及xml配置、jsp页面、sql脚本、docx与md文档等,结构完整,覆盖后端逻辑、前端界面与数据库设计。目前已有222人学习下载,适合需要完整赛题方案、源码分析素材与部署排错思路的读者参考,也可作为软件工程实践与Android应用开发的实战练习。
1. 老年人服药提醒 App:从一份毕业设计源码看移动端健康提醒的完整落地
很多同学做毕业设计时,选题第一反应是"做个 App",但真正能跑通、能答辩、能写进简历的,往往是那些把某个具体场景做透的项目。老年人服药提醒 App 就是这样一个典型:它看起来只是"定时弹个通知",但真做起来,你会碰到本地闹钟在国产 ROM 上被系统杀掉、跨天服药计划怎么算、漏服后如何补提醒、家属如何远程看到老人有没有吃药这一连串问题。这份源码要解决的,正是把"提醒"这件事从一句需求变成一套可运行、可演示、可扩展的移动端方案。它适合正在做 Java/Android 方向毕业设计的同学,也适合想入门移动端本地通知与数据持久化的开发者。下面我按"先讲清结构、再动手复现、最后说坑"的顺序,把这类项目拆开讲透。
2. 先看清这套源码的骨架:模块划分与技术选型
拿到一份"老年人服药提醒 App"的源码,别急着点运行。先花二十分钟把目录结构和依赖理清楚,后面改需求、写论文、答辩演示都会顺很多。这类项目通常是一个单模块 Android 工程,核心逻辑集中在几个 Activity、一个数据库帮助类、一个闹钟调度类和一组广播接收器里。
2.1 典型目录结构与各模块职责
一个能跑通的服药提醒工程,目录大致长这样(不同实现会有出入,但职责划分基本一致):
app/src/main/java/com/example/medreminder/ ├── MainActivity.java // 首页:今日服药清单 ├── AddMedicineActivity.java // 添加/编辑药品与服药计划 ├── MedicineListActivity.java // 全部药品列表 ├── db/ │ ├── DBHelper.java // SQLite 建表与版本管理 │ └── MedicineDao.java // 增删改查封装 ├── model/ │ ├── Medicine.java // 药品实体 │ └── DoseRecord.java // 服药记录实体 ├── alarm/ │ ├── AlarmScheduler.java // 计算并注册闹钟 │ └── AlarmReceiver.java // 接收闹钟广播,发通知 └── notify/ └── NotificationHelper.java // 通知渠道与样式这里最关键的是把"数据"和"调度"分开。Medicine存的是"这个药一天吃几次、每次几片、从哪天开始吃";DoseRecord存的是"某天某次到底吃没吃"。很多人一开始把两者混在一张表里,结果一到"漏服补提醒"就彻底乱套。分开之后,今日清单 = 根据 Medicine 的计划算出今天该吃的所有时间点,再和 DoseRecord 做左连接,没记录的显示"待服用"。
2.2 技术选型:为什么用 SQLite + AlarmManager 而不是 Room + WorkManager
毕业设计阶段,选型的第一原则是"能讲清楚、能调试、不依赖网络"。SQLite 原生 API 虽然啰嗦,但胜在零依赖、SQL 语句直观,答辩时老师问"数据怎么存的"你能直接指着建表语句讲。Room 更现代,但注解处理器一旦版本对不上,编译报错能卡你一整天。
闹钟调度同理。AlarmManager是系统级 API,配合setExactAndAllowWhileIdle能在低电耗模式下也尽量准时触发,适合"到点必须提醒"的场景。WorkManager 更适合"可以晚几分钟"的后台任务,而且它的最小周期是 15 分钟,做精确到分钟的服药提醒并不合适。所以这类项目的常见做法是:AlarmManager 负责精确触发,WorkManager 只用来做"每天凌晨重算第二天闹钟"这种粗粒度任务。
提示:选型没有绝对对错,关键是你能说清"为什么不用另一个"。答辩时被问到 WorkManager,能答出"最小周期 15 分钟不满足服药精度"就已经赢了。
2.3 数据库表设计:三张表撑起整个业务
把表设计好,后面写代码就是填空题。核心三张表:
| 表名 | 关键字段 | 作用 |
|---|---|---|
| medicine | id, name, dosage, times_per_day, start_date, end_date | 药品与服药计划 |
| dose_time | id, medicine_id, hour, minute | 每种药的具体服药时间点 |
| dose_record | id, medicine_id, plan_time, status, taken_at | 每次服药的执行记录 |
dose_time单独拆出来,是因为"一天三次"背后是三个具体时间点(比如 8:00、12:30、18:00),拆开后计算今日清单就是一次简单的按时间排序查询。dose_record的status用整数表示:0 待服用、1 已服用、2 已跳过、3 漏服。状态机清晰,后面做统计和补提醒都方便。
3. 动手复现:从建表到闹钟触发的完整链路
这一章是能直接抄作业的部分。我按"数据层 → 调度层 → 通知层"的顺序给代码,每段都说明参数怎么改、失败时看哪里。你照着敲一遍,基本就能跑出一个能演示的最小版本。
3.1 建表与 DAO:把服药计划写进 SQLite
先看建表。DBHelper继承SQLiteOpenHelper,在onCreate里执行建表语句:
public class DBHelper extends SQLiteOpenHelper { private static final String DB_NAME = "med_reminder.db"; private static final int DB_VERSION = 1; public DBHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } @Override public void onCreate(SQLiteDatabase db) { // 药品表:记录药品基本信息和服药周期 db.execSQL("CREATE TABLE medicine (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "name TEXT NOT NULL," + "dosage TEXT," + "times_per_day INTEGER DEFAULT 1," + "start_date TEXT," + "end_date TEXT)"); // 服药时间点表:一种药对应多个时间点 db.execSQL("CREATE TABLE dose_time (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "medicine_id INTEGER," + "hour INTEGER," + "minute INTEGER)"); // 服药记录表:每次计划对应一条记录 db.execSQL("CREATE TABLE dose_record (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "medicine_id INTEGER," + "plan_time TEXT," + // 格式 yyyy-MM-dd HH:mm "status INTEGER DEFAULT 0," + "taken_at TEXT)"); } @Override public void onUpgrade(SQLiteDatabase db, int oldV, int newV) { // 毕业设计阶段简单处理:直接重建 db.execSQL("DROP TABLE IF EXISTS medicine"); db.execSQL("DROP TABLE IF EXISTS dose_time"); db.execSQL("DROP TABLE IF EXISTS dose_record"); onCreate(db); } }DB_VERSION从 1 开始,以后加字段就把它 +1 并在onUpgrade里写迁移逻辑。这里为了演示直接重建,真实项目里千万别这么干,老人攒了半年的服药记录一次全没了,那是血泪教训。
接着封装一个查询"今日待服用清单"的方法:
public List<DoseRecord> queryTodayPlan(String today) { List<DoseRecord> list = new ArrayList<>(); // 关联三张表:按药品周期过滤,按时间点展开,左连接记录表 String sql = "SELECT m.id, m.name, m.dosage, dt.hour, dt.minute, " + "IFNULL(r.status, 0) AS status " + "FROM medicine m " + "JOIN dose_time dt ON dt.medicine_id = m.id " + "LEFT JOIN dose_record r ON r.medicine_id = m.id " + "AND r.plan_time = ? || ' ' || printf('%02d:%02d', dt.hour, dt.minute) " + "WHERE ? BETWEEN m.start_date AND m.end_date " + "ORDER BY dt.hour, dt.minute"; Cursor c = getReadableDatabase().rawQuery(sql, new String[]{today, today}); while (c.moveToNext()) { DoseRecord d = new DoseRecord(); d.medicineId = c.getInt(0); d.name = c.getString(1); d.dosage = c.getString(2); d.hour = c.getInt(3); d.minute = c.getInt(4); d.status = c.getInt(5); list.add(d); } c.close(); return list; }这段 SQL 是整套逻辑的核心。printf('%02d:%02d', ...)把小时分钟拼成08:00这种格式,和plan_time对齐;LEFT JOIN保证即使还没服药记录,计划也能查出来,IFNULL把空状态兜底成 0。参数today传yyyy-MM-dd字符串,注意别传成带时间的,否则BETWEEN比较会出错。
3.2 闹钟调度:用 AlarmManager 注册精确提醒
数据有了,接下来是"到点响"。核心思路是:每次打开 App 或服药状态变化时,重算未来 24 小时内所有待服用的时间点,逐个注册闹钟。
public class AlarmScheduler { public static void scheduleAll(Context ctx, List<DoseRecord> todayPlan) { AlarmManager am = (AlarmManager) ctx.getSystemService(Context.ALARM_SERVICE); long now = System.currentTimeMillis(); for (int i = 0; i < todayPlan.size(); i++) { DoseRecord d = todayPlan.get(i); if (d.status != 0) continue; // 已服用/已跳过的不再提醒 Calendar cal = Calendar.getInstance(); cal.set(Calendar.HOUR_OF_DAY, d.hour); cal.set(Calendar.MINUTE, d.minute); cal.set(Calendar.SECOND, 0); long triggerAt = cal.getTimeInMillis(); if (triggerAt <= now) continue; // 今天已过的时间点跳过 Intent intent = new Intent(ctx, AlarmReceiver.class); intent.putExtra("medicine_id", d.medicineId); intent.putExtra("name", d.name); intent.putExtra("dosage", d.dosage); // requestCode 用 medicineId*100 + 序号,保证每个闹钟唯一 int reqCode = d.medicineId * 100 + i; PendingIntent pi = PendingIntent.getBroadcast( ctx, reqCode, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); // 精确闹钟,低电耗下也尽量触发 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAt, pi); } else { am.setExact(AlarmManager.RTC_WAKEUP, triggerAt, pi); } } } }几个参数必须说清:RTC_WAKEUP表示用绝对时间且能唤醒设备,服药提醒必须用它,用ELAPSED_REALTIME会在设备重启后错乱。requestCode是闹钟的唯一标识,如果所有闹钟都用同一个值,后注册的会覆盖前面的,这是新手最常翻的车。FLAG_IMMUTABLE在 Android 12 以后是强制的,不加会直接崩。
AlarmReceiver收到广播后,调用通知助手弹出提醒:
public class AlarmReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { int medId = intent.getIntExtra("medicine_id", -1); String name = intent.getStringExtra("name"); String dosage = intent.getStringExtra("dosage"); NotificationHelper.show(context, medId, name, dosage); } }3.3 通知渠道与点击跳转:让提醒真正"可操作"
Android 8.0 以后通知必须走渠道,不建渠道通知直接不显示。NotificationHelper大致如下:
public class NotificationHelper { private static final String CHANNEL_ID = "med_reminder_channel"; public static void show(Context ctx, int medId, String name, String dosage) { NotificationManager nm = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE); // 8.0 以上必须创建渠道,否则通知静默失败 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel ch = new NotificationChannel( CHANNEL_ID, "服药提醒", NotificationManager.IMPORTANCE_HIGH); ch.setDescription("到点提醒老人服药"); nm.createNotificationChannel(ch); } // 点击通知回到首页 Intent open = new Intent(ctx, MainActivity.class); open.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); PendingIntent contentPi = PendingIntent.getActivity( ctx, medId, open, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); Notification n = new NotificationCompat.Builder(ctx, CHANNEL_ID) .setSmallIcon(R.drawable.ic_pill) .setContentTitle("该吃药了:" + name) .setContentText("用量:" + dosage) .setPriority(NotificationCompat.PRIORITY_HIGH) .setAutoCancel(true) .setContentIntent(contentPi) .build(); nm.notify(medId, n); } }IMPORTANCE_HIGH决定通知会横幅弹出并响铃,做提醒类应用必须用 HIGH,用 DEFAULT 老人根本注意不到。notify的第一个参数用medId,保证同一种药的通知会覆盖而不是堆叠。
3.4 权限清单:漏一个就白干
Android 6.0 以后,精确闹钟、通知、开机自启都需要在清单里声明,部分还要运行时申请:
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>SCHEDULE_EXACT_ALARM在 Android 12 以后要引导用户去系统设置里手动开,POST_NOTIFICATIONS在 Android 13 以后要运行时申请。这两个不处理,演示时通知不弹,你会以为是代码写错了,其实是权限没给。
4. 避坑与排查:那些让演示当场翻车的细节
代码能编译不代表能演示。下面这几条是我踩过、也见过别人踩的坑,按"现象 → 原因 → 解决"列出来,你对着排查能省下大量时间。
4.1 通知到点不弹,锁屏后尤其明显
现象:App 在前台时通知正常,一锁屏或切后台就再也不响。原因:国产 ROM 对后台应用有严格的省电限制,setExactAndAllowWhileIdle也会被拦截,加上应用被"冻结"后广播收不到。解决:引导用户在系统设置里把应用加入"电池优化白名单"和"自启动白名单",代码里可以用PowerManager.isIgnoringBatteryOptimizations检测并跳转设置页。演示机最好提前手动设置好,别指望现场教老师操作。
4.2 重启手机后所有闹钟消失
现象:手机重启,当天剩下的服药提醒全没了。原因:AlarmManager注册的闹钟不持久化,重启即清空。解决:注册一个监听ACTION_BOOT_COMPLETED的广播接收器,开机后重新调用AlarmScheduler.scheduleAll。注意这个接收器要在清单里静态注册,并在 Android 8.0 以后确认没被后台限制。
4.3 同一种药多个时间点只响一次
现象:一天三次的药,只有第一次提醒。原因:PendingIntent的requestCode重复,后注册的覆盖了前面的。解决:如 3.2 节所示,用medicineId * 100 + 序号保证唯一。序号建议用时间点在列表里的下标,别用小时数,否则同小时的两个时间点还是会撞。
4.4 跨天计划算错,凌晨提醒乱飞
现象:晚上添加的药,第二天凌晨突然提醒。原因:计算今日清单时用了Calendar默认时区,或者plan_time拼接时没补零,导致08:00被存成8:0。解决:统一用SimpleDateFormat("yyyy-MM-dd HH:mm")格式化,拼接小时分钟时用String.format("%02d:%02d", h, m)。跨天判断用日期字符串比较,别用时间戳相减。
4.5 数据库升级把老人记录清空
现象:改了个字段,重新安装后历史服药记录全没了。原因:onUpgrade里写了DROP TABLE。解决:正式版本必须写ALTER TABLE ADD COLUMN迁移,或者用CREATE TABLE new+INSERT SELECT+DROP old+RENAME的标准迁移流程。毕业设计演示前,把DB_VERSION固定住,别在答辩前一天改表结构。
5. 进阶技巧:让这份毕业设计从"能跑"到"能打"
如果基础版本已经跑通,想让项目在答辩时更有说服力,可以从两个方向加码。一是把"漏服补提醒"做成一个可演示的闭环:在AlarmReceiver里判断当前时间距离计划时间是否超过阈值(比如 30 分钟),超过就标记为漏服并触发一次"补提醒",同时在首页用红色高亮。这个逻辑不复杂,但能体现你对真实场景的思考,老师一问"老人没听到怎么办"你就有话说。
二是加一个极简的统计页,用MPAndroidChart画一张近七天的服药完成率柱状图。数据直接来自dose_record的status字段,一条GROUP BY date(plan_time)的 SQL 就能出结果。图表不用花哨,能看出"哪天漏了"就够。这一步的价值在于,它把项目从"单点提醒"升级成了"服药管理",论文里的"系统功能"章节立刻充实起来。
-- 近七天服药完成率:已服用数 / 计划总数 SELECT date(plan_time) AS day, SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) * 1.0 / COUNT(*) AS rate FROM dose_record WHERE plan_time >= date('now', '-7 days') GROUP BY day ORDER BY day;这条 SQL 里* 1.0是为了让整数除法变成浮点除法,不加的话结果永远是 0 或 1,这个坑我当年调了半小时才反应过来。date('now', '-7 days')是 SQLite 的日期函数,注意它按 UTC 算,如果对时区敏感,得手动加偏移。
最后说个习惯:每次改完调度相关代码,别只在模拟器上点两下就完事。把系统时间手动调到提醒前两分钟,锁屏,等它响。这一步能提前暴露九成的通知问题。我做这类项目时,答辩前一晚一定会用真机把"添加药品 → 到点提醒 → 点击已服用 → 首页状态更新"这条链路完整走三遍,走顺了才敢睡。希望帮到你。
本文还有配套的精品资源,点击获取