简介:这份资源是面向Android初学者与进阶开发者的职工考勤APP完整工程包,基于Android Studio构建,围绕员工信息管理、考勤录入、统计报表、异常提醒、数据可视化与权限分级等核心业务场景,帮助读者理解一款企业级考勤工具从需求到落地的实现思路。压缩包共547个文件,约14.88MB,涵盖java源码、xml布局、json配置、dex与class编译产物、jar依赖、gradle构建脚本及properties配置等,完整保留了Android项目的目录结构与构建痕迹,便于对照学习工程组织方式。目前已有693人学习下载。读者可从中获取SQLite数据库操作、Material Design界面设计、Notification通知服务、MPAndroidChart图表展示以及权限管理等模块的实践参考,适合作为课程设计、毕业设计或自学练手项目的对照素材,也可用于排查构建配置与资源组织中的常见问题。
1. 从一份安卓职工考勤APP源码包说起:它能帮你省掉多少重复造轮子的时间
如果你正在用 Android Studio 做课程设计、公司内部小工具,或者想找一个能跑通的移动端增删改查项目练手,那这份「安卓职工考勤APP.7z」大概率能让你少熬两个晚上。它不是一个空壳 Demo,而是围绕「员工信息管理 + 考勤记录录入 + 统计汇总 + 通知提醒 + 图表展示 + 权限分级」这条完整业务线搭出来的 Android 工程。解压后你会看到标准的 Gradle 目录结构、app/src/main下的 Activity 与布局文件、SQLite 建表脚本,以及资源目录里那串androidResources对应的图标与主题配置。适合谁?一是刚学完 Android 基础、需要真实项目串联知识点的开发者;二是被要求「两周内交一个考勤 App」的倒霉蛋;三是想看看别人怎么把 SQLite、Notification、MPAndroidChart 揉进一个工程里的熟手。下面我按实际拆包和复现的顺序,把这份资源从导入到跑通、再到改参数和排错,一层层拆开讲。
2. 导入 Android Studio 与工程结构速览:先让项目跑起来再谈改代码
2.1 解压后的目录该看哪几个地方
拿到.7z之后,先解压到一个纯英文、无空格的路径下,比如D:\work\attendance_demo。中文路径在 Gradle 同步阶段偶尔会触发资源合并失败,这是血泪经验,不是玄学。解压完用 Android Studio 的「Open」指向工程根目录,不要用「Import Project」,后者对老版本 Gradle 包装器容易误判。
工程根目录下重点看四个位置:
settings.gradle:确认include ':app'存在,说明是单模块工程,没有多余的 library 模块拖慢同步。app/build.gradle:这里决定compileSdk、minSdk、targetSdk以及依赖库版本。考勤类 App 通常minSdk设在 21 或 23,因为要用到NotificationChannel(API 26 引入)和运行时权限(API 23 引入)。app/src/main/AndroidManifest.xml:权限声明、Activity 注册、Application 类入口都在这里。考勤 App 常见权限包括INTERNET(云同步)、ACCESS_NETWORK_STATE、POST_NOTIFICATIONS(Android 13+ 通知权限)。app/src/main/java/...:包名下的 Activity、Adapter、数据库帮助类、工具类。一般会有MainActivity、EmployeeActivity、AttendanceActivity、StatisticsActivity、DBHelper、NotificationHelper这几个核心文件。
提示:如果解压后看到
.idea和.gradle缓存目录,直接删掉再打开,让 Android Studio 重新生成,能避免大部分「找不到 SDK」的报错。
2.2 Gradle 同步失败的三个常见卡点
第一次同步大概率不会一次过。我一般按下面顺序排查:
卡点一:Gradle 版本与 JDK 不匹配。工程里gradle-wrapper.properties指定的 Gradle 版本如果太老(比如 6.x),而你本机 Android Studio 自带 JDK 17,就会报Unsupported class file major version。解决办法是改 wrapper 版本到 7.5 以上,或者把 Android Studio 的 Gradle JDK 切到 11。
卡点二:依赖仓库访问慢。build.gradle里如果只写了google()和mavenCentral(),国内网络下同步依赖会非常慢。常见做法是在settings.gradle的pluginManagement和dependencyResolutionManagement里加国内镜像源。注意,这里只改仓库地址,不要动依赖本身的版本号。
卡点三:compileSdk与本地 SDK 不匹配。如果工程写的是compileSdk 33,而你只装了 34,同步会提示缺少平台。打开 SDK Manager 勾选对应版本即可,不要盲目把compileSdk改成你已有的版本,因为部分 API 调用在低版本上会编译不过。
// app/build.gradle 中需要重点确认的参数 android { compileSdk 33 // 与本地已安装 SDK 平台保持一致 defaultConfig { applicationId "com.example.attendance" minSdk 23 // 低于 23 运行时权限模型不完整 targetSdk 33 // 影响通知权限和后台限制行为 versionCode 1 versionName "1.0" } } dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.9.0' implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' // 图表库 }上面这段配置里,minSdk 23是考勤 App 的合理下限,因为你要在代码里动态申请存储和通知权限;targetSdk建议跟compileSdk对齐,否则 Android 13 以上设备通知弹不出来。MPAndroidChart 的依赖如果同步不下来,可以在settings.gradle里加maven { url 'https://jitpack.io' }。
2.3 跑通第一个界面:从启动 Activity 到数据初始化
同步成功后,先别急着点 Run。打开DBHelper看一眼建表逻辑,确认数据库名和版本号。考勤 App 通常建三张表:employee(员工信息)、attendance(考勤记录)、user(登录账号与权限)。如果onCreate里只建了表但没有插入初始管理员账号,你登录时会卡在空白页。
// DBHelper.java 中典型的建表与初始数据逻辑 public class DBHelper extends SQLiteOpenHelper { private static final String DB_NAME = "attendance.db"; private static final int DB_VERSION = 1; @Override public void onCreate(SQLiteDatabase db) { // 员工表:工号作为唯一标识 db.execSQL("CREATE TABLE employee (" + "emp_id TEXT PRIMARY KEY, " + "name TEXT NOT NULL, " + "department TEXT, " + "position TEXT)"); // 考勤表:一条记录对应一次打卡事件 db.execSQL("CREATE TABLE attendance (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "emp_id TEXT, " + "check_time TEXT, " + "check_type INTEGER, " + // 0 上班 1 下班 "status INTEGER)"); // 0 正常 1 迟到 2 早退 3 请假 // 初始管理员账号,避免首次登录无账号可用 db.execSQL("INSERT INTO user (username, password, role) " + "VALUES ('admin', '123456', 1)"); } }这段代码里,check_type用整数区分上下班,status用整数区分考勤状态,这是移动端 SQLite 的常规做法,比存字符串省空间也方便统计。emp_id作为主键而不是自增 id,是为了跟企业工号对齐,导入导出时不容易乱。建完表后,App 启动时MainActivity会读user表判断是否已登录,没登录就跳登录页,登录后按role字段决定进管理员界面还是普通员工界面。
3. 考勤核心逻辑拆解:打卡时间判定、统计计算与通知触发
3.1 上下班时间判定与迟到早退算法
考勤 App 最核心也最容易翻车的地方,就是「几点算迟到、几点算早退」。这份资源里通常会在Constants或Config类里定义上下班时间阈值,然后在插入考勤记录时做比较。常见做法是:上班时间设为 09:00,允许 15 分钟宽限,09:15 之后打卡标记为迟到;下班时间设为 18:00,17:45 之前打卡标记为早退。
// AttendanceUtil.java 中的时间判定逻辑 public static int judgeStatus(String checkTime, int checkType) { SimpleDateFormat sdf = new SimpleDateFormat("HH:mm", Locale.CHINA); try { Date check = sdf.parse(checkTime); Date workStart = sdf.parse("09:00"); Date workEnd = sdf.parse("18:00"); long graceMillis = 15 * 60 * 1000L; // 15 分钟宽限 if (checkType == 0) { // 上班打卡 if (check.getTime() > workStart.getTime() + graceMillis) { return 1; // 迟到 } } else { // 下班打卡 if (check.getTime() < workEnd.getTime() - graceMillis) { return 2; // 早退 } } } catch (ParseException e) { return 0; // 解析失败按正常处理,避免崩溃 } return 0; // 正常 }这里有几个参数你需要按自己场景改:workStart、workEnd、graceMillis。如果是多班次企业,就不能用固定字符串,得从数据库读班次配置。另外注意SimpleDateFormat不是线程安全的,如果放在异步任务里并发调用,建议换成ThreadLocal或者java.time.LocalTime(API 26+)。解析失败返回 0 是一种兜底策略,防止一条脏数据导致整个打卡流程崩溃。
3.2 考勤统计的 SQL 聚合与图表数据准备
统计页面一般展示「出勤天数、迟到次数、早退次数、请假次数」,这些数据不需要在 Java 里遍历计算,直接用 SQL 聚合更稳。StatisticsActivity里通常按员工工号和月份筛选,然后调DBHelper的查询方法。
-- 按员工和月份统计各类考勤次数 SELECT e.emp_id, e.name, SUM(CASE WHEN a.status = 0 THEN 1 ELSE 0 END) AS normal_count, SUM(CASE WHEN a.status = 1 THEN 1 ELSE 0 END) AS late_count, SUM(CASE WHEN a.status = 2 THEN 1 ELSE 0 END) AS early_count, SUM(CASE WHEN a.status = 3 THEN 1 ELSE 0 END) AS leave_count FROM employee e LEFT JOIN attendance a ON e.emp_id = a.emp_id WHERE a.check_time LIKE '2024-06%' GROUP BY e.emp_id, e.name;这条 SQL 用CASE WHEN把不同状态映射成计数列,LEFT JOIN保证没有考勤记录的员工也能出现在结果里(计数为 0)。LIKE '2024-06%'是按月份过滤的简单写法,如果数据量大,建议把check_time存成时间戳整数,用BETWEEN查询走索引更快。查出来的结果直接喂给 MPAndroidChart 的BarEntry列表,X 轴放员工姓名,Y 轴放出勤天数或迟到次数。
3.3 通知提醒的触发时机与权限适配
通知功能在考勤 App 里通常有两个触发点:一是打卡成功后本地提醒「打卡成功,时间 09:03」;二是定时任务扫描当天未打卡员工,在固定时间发提醒。这份资源里大概率用的是AlarmManager+BroadcastReceiver,而不是WorkManager,因为前者在国产 ROM 上保活策略更可控。
// NotificationHelper.java 中创建通知渠道并发送提醒 public static void sendNotification(Context context, String title, String content) { NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); String channelId = "attendance_remind"; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel channel = new NotificationChannel( channelId, "考勤提醒", NotificationManager.IMPORTANCE_HIGH); manager.createNotificationChannel(channel); } NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId) .setSmallIcon(R.drawable.ic_notify) .setContentTitle(title) .setContentText(content) .setPriority(NotificationCompat.PRIORITY_HIGH) .setAutoCancel(true); // Android 13+ 需要动态申请 POST_NOTIFICATIONS 权限 if (Build.VERSION.SDK_INT >= 33 && ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { return; // 无权限直接返回,避免 SecurityException } manager.notify((int) System.currentTimeMillis(), builder.build()); }channelId一旦创建后,通知的重要性等级就不能通过代码修改了,用户只能在系统设置里改。所以第一次建渠道时就要想好IMPORTANCE_HIGH还是DEFAULT。另外 Android 13 的POST_NOTIFICATIONS权限必须在 Activity 里用ActivityResultLauncher申请,不能只在 Manifest 里声明。
4. 避坑与排查:导入、运行、统计三个环节的翻车记录
4.1 坑一:Gradle 同步报「Could not find method implementation()」
现象:打开工程后 Gradle 同步失败,提示在build.gradle里找不到implementation方法。原因:工程用的 Gradle 插件版本太老,implementation配置在 Gradle 3.0 以下不存在,或者build.gradle里误把dependencies块写在了android块内部。解决:检查根目录build.gradle的com.android.tools.build:gradle版本,低于 3.0 就升到 4.0 以上;同时确认dependencies块与android块平级,不要嵌套。
4.2 坑二:登录后闪退,日志报「no such table: user」
现象:App 能启动到登录页,输入账号密码点登录后直接闪退,Logcat 显示SQLiteException: no such table: user。原因:DBHelper的onCreate只在数据库第一次创建时执行,如果你之前跑过一次旧版本,数据库已经存在但没建user表,onCreate不会再触发。解决:把DB_VERSION加 1,并在onUpgrade里补建缺失的表;或者直接卸载重装 App,让数据库重新初始化。开发阶段我一般会加一句context.deleteDatabase("attendance.db")在调试入口,省得反复卸载。
4.3 坑三:统计图表空白,但 SQL 在 DB Browser 里能查到数据
现象:统计页面打开后图表区域一片白,没有柱状图,但把同样的 SQL 放到数据库工具里执行有结果。原因:MPAndroidChart 的BarEntry构造参数是(x, y),如果 x 值重复或者没有按升序排列,图表渲染会异常;另一种情况是查询返回的Cursor没有移动到第一行之前就调用了getCount。解决:确保 X 轴值唯一且递增,Cursor使用前先moveToFirst();如果数据量少于 1 条,直接隐藏图表并显示「暂无数据」文本,不要传空列表给Chart.setData()。
4.4 坑四:Android 13 设备上通知不弹,权限已声明
现象:Manifest 里写了POST_NOTIFICATIONS,但 Android 13 真机上打卡后没有任何通知。原因:Android 13 把通知权限改成了运行时权限,只在 Manifest 声明不够,必须在代码里动态申请,且用户拒绝后不会再自动弹窗。解决:在MainActivity的onCreate里用registerForActivityResult申请权限,并在用户拒绝后引导到系统设置页手动开启。注意不要用shouldShowRequestPermissionRationale做强制循环申请,体验很差。
4.5 坑五:考勤时间存成字符串后,跨月查询结果错乱
现象:查询 6 月数据时,5 月 31 日的记录也被查出来了。原因:check_time存的是yyyy-MM-dd HH:mm格式字符串,用LIKE '2024-06%'过滤时,如果某条记录格式不统一(比如2024-6-1没有补零),就会漏掉或误匹配。解决:统一用SimpleDateFormat("yyyy-MM-dd HH:mm")格式化后再入库,查询时用BETWEEN '2024-06-01 00:00' AND '2024-06-30 23:59',比LIKE更精确。长期方案是存时间戳整数。
5. 进阶改造与验证:把考勤数据导出成 CSV 并做一次完整回归
5.1 用 CSV 导出替代截图汇报
很多考勤 App 做到统计图表就停了,但实际用起来,管理层要的是能贴进 Excel 的明细。我一般会在StatisticsActivity加一个「导出」按钮,把查询结果写成 CSV 放到getExternalFilesDir下,再用FileProvider分享出去。这样不用申请存储权限,也能在 Android 10 以上正常工作。
// CsvExporter.java 核心逻辑 public static File export(Context context, List<AttendanceRecord> records) throws IOException { File dir = context.getExternalFilesDir(null); File csv = new File(dir, "attendance_" + System.currentTimeMillis() + ".csv"); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(csv), StandardCharsets.UTF_8)); writer.write("工号,姓名,日期,打卡时间,状态\n"); // BOM 可选,防 Excel 乱码 for (AttendanceRecord r : records) { writer.write(r.getEmpId() + "," + r.getName() + "," + r.getDate() + "," + r.getTime() + "," + r.getStatusText() + "\n"); } writer.flush(); writer.close(); return csv; }注意StandardCharsets.UTF_8要显式指定,否则 Windows 上默认 GBK,Excel 打开会乱码。如果要在 Excel 里直接双击打开不乱码,可以在文件开头写\uFEFFBOM。getExternalFilesDir不需要额外权限,但路径较深,分享时用FileProvider.getUriForFile生成 content URI。
5.2 回归验证清单:改完代码后至少跑这五步
每次改完考勤判定逻辑或数据库结构,我会按下面顺序过一遍,避免改 A 坏 B:
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 卸载重装 App | 数据库重新创建,无旧表残留 |
| 2 | 用 admin 登录 | 进入管理员界面,能看到员工列表 |
| 3 | 新增一名员工并打卡 | 考勤表插入一条记录,状态正确 |
| 4 | 修改系统时间到 09:20 再打卡 | 状态标记为迟到 |
| 5 | 打开统计页并导出 CSV | 图表有数据,CSV 能用 Excel 打开 |
这五步覆盖了数据库初始化、权限、时间判定、统计查询和文件导出,基本能拦住大部分回归问题。尤其是第 4 步,改时间判定参数后必须手动验证,不要只靠单元测试,因为SimpleDateFormat的时区行为在不同设备上可能不一致。
5.3 一个让我长记性的习惯
早些年我改考勤状态字段时,直接把status的 0/1/2/3 含义换了顺序,结果旧数据全部错位,统计报表把迟到算成了正常。从那以后我每次动枚举值或者状态码,都强制走一遍「改常量 → 加数据库版本 → 写迁移脚本 → 跑回归清单」这四步,哪怕只是改一个数字。这份安卓职工考勤APP的源码结构不算复杂,但业务逻辑的坑往往不在代码本身,而在数据一致性和版本迁移上。希望帮到你。
本文还有配套的精品资源,点击获取