
简介这是一份面向高校计算机专业学生与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 数据库表设计:三张表撑起整个业务把表设计好,后面写代码就是填空题。核心三张表:表名关键字段作用medicineid, name, dosage, times_per_day, start_date, end_date药品与服药计划dose_timeid, medicine_id, hour, minute每种药的具体服药时间点dose_recordid, 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 ListDoseRecord queryTodayPlan(String today) { ListDoseRecord 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, ListDoseRecord 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:nameandroid.permission.SCHEDULE_EXACT_ALARM/ uses-permission android:nameandroid.permission.POST_NOTIFICATIONS/ uses-permission android:nameandroid.permission.RECEIVE_BOOT_COMPLETED/ uses-permission android:nameandroid.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 newINSERT SELECTDROP oldRENAME的标准迁移流程。毕业设计演示前,把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 算,如果对时区敏感,得手动加偏移。最后说个习惯:每次改完调度相关代码,别只在模拟器上点两下就完事。把系统时间手动调到提醒前两分钟,锁屏,等它响。这一步能提前暴露九成的通知问题。我做这类项目时,答辩前一晚一定会用真机把添加药品 → 到点提醒 → 点击已服用 → 首页状态更新这条链路完整走三遍,走顺了才敢睡。希望帮到你。本文还有配套的精品资源点击获取