Android人事管理系统开发:考勤打卡、请假审批与离线缓存
2026/9/17 12:11:20 网站建设 项目流程

简介:本资源为基于Android的人事管理系统毕业设计资料包,面向计算机相关专业的本科毕业生及需要移动端开发练手的初学者。内容包含完整毕业论文与配套源码,论文围绕Android平台下企业人事管理的移动化改造展开,解决传统人工管理模式效率低、数据分散的问题,覆盖系统开发工具、JAVA、MYSQL、Android编程与JSP等关键技术,并依次梳理需求与可行性分析、数据库概要设计与实体模型、登录及主页、员工管理、部门管理、职位管理等模块的实现思路,最后给出系统调试与功能测试过程,可帮助读者对照选题背景、技术选型、数据库表结构到测试方法完整梳理一遍开发流程,用于论文撰写参考或二次开发均较方便。压缩包共1个文件,类型为doc,包体约1.11MB,篇幅紧凑便于通读。目前已有185人学习浏览,适合作为Android方向毕业设计的参考样例。

1. 考勤、请假、档案:Android 人事管理系统真正难在哪

人事管理系统常被当成几张表的增删改查,套个后台模板就能交差。把运行环境换成 Android,问题立刻变形:员工在地铁里点请假、在厂区门口打卡、在信号很差的仓库里填调岗申请。脱离内网之后,请求超时、定位漂移、重复提交、Token 过期会同时冒出来,而这恰恰是评审最容易追问的地方。

核心链路只有三条——员工档案查询、考勤打卡、请假审批。每条都要处理网络、权限、缓存和状态同步。往下按需求边界、工程骨架、核心模块、源码整理的顺序展开,Android 端代码以 Java 配合 Android Studio 为主,服务端只约定 HTTP 接口与 JSON 结构,后端用 Spring Boot、SSM 还是别的框架都能对接。

2. Android 人事管理系统的需求拆解与选型理由

2.1 先划清四类角色和用例边界

做移动端之前必须回答一个问题:哪些功能值得放到手机上。把 PC 端后台的菜单原样搬到 Android,最后会得到一个又长又难用的列表,功能看着齐全,实际没人点。我一般的做法是把用例按角色切开,只把「离开工位才会发生」的操作留在移动端,其余继续留在后台。

角色移动端核心用例是否必须上手机
普通员工打卡、请假申请、查工资条与考勤明细、改联系方式必须,高频
部门主管待办审批、团队考勤概览、驳回并填意见必须,强时效
HR 专员员工档案增改、入职离职办理、导出报表部分,查询为主
系统管理员角色授权、部门树维护、接口配置不建议,后台更合适

这张表决定了后面所有接口的粒度。员工端只暴露自己的数据,主管端多一层「下属范围」的过滤条件,HR 端才允许跨部门查询。权限边界放在服务端做,Android 端只负责按角色渲染入口,不要指望客户端能拦住越权请求。

2.2 移动端为什么优先选原生 Android

毕设和中小型企业内部应用,我建议老老实实用原生 Android,而不是一上来就跨平台框架。理由很实际:打卡要拿定位和传感器、要读系统时间做校验、要在后台被杀死后恢复,这些能力原生 API 最直接,出了问题也容易定位。跨平台方案在写论文时讲不清「Android 特性体现在哪」,答辩时反而被动。

工程配置上有几个值要提前定死。minSdkVersion决定你能用哪些 API,也决定兼容测试的范围;targetSdkVersion影响权限行为和后台限制,版本越高系统约束越严;权限必须按需申请,不要在启动页一次性弹完。清单文件里典型的声明如下。

<!-- AndroidManifest.xml --> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- 打卡定位:先要粗略,再要精确,按需申请 --> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <!-- 上传头像、导出考勤凭证时读取相册/相机 --> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <application android:name=".App" android:usesCleartextTraffic="false" android:networkSecurityConfig="@xml/network_security_config"> <!-- 7.0 起跨应用传文件必须走 FileProvider,头像上传就靠它 --> <provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider> </application>

usesCleartextTraffic设成 false 是硬要求,正式环境全走 HTTPS。FileProvider那段看着多余,但只要你做头像上传或导出 PDF 考勤单,没有它就会在 Android 7.0 以上直接抛FileUriExposedException,这也是很多移植过来的老项目一跑就崩的原因。

2.3 服务端接口契约与数据库表的一一对应

Android 端最怕接口反复改。我的习惯是先写接口表,再写客户端代码,字段名一旦确定就不动。下面这张表覆盖三条主链路,实际开发时按它生成 Retrofit 接口即可。

接口路径方法关键入参返回要点
/api/auth/loginPOSTaccount、password、deviceIdtoken、过期时间、角色
/api/employee/pageGETkeyword、deptId、page、size列表、总数
/api/attendance/clockPOSTtype、longitude、latitude、clientTime服务端时间、打卡状态
/api/leave/applyPOSTstartTime、endTime、reason、requestId审批单号
/api/leave/approvePOSTleaveId、action、comment最新状态

客户端架构上我不太推荐把逻辑全塞进 Activity。常见的稳妥组合是:网络层用单例的 Retrofit 实例(单例模式,避免重复创建连接池),数据层用 Repository 收口,界面层订阅可观察的数据源。用 Java 写就是 LiveData 或 RxJava,用 Kotlin 就是 Flow,本质都是观察者模式。分层带来的直接好处是换 UI 不用动网络代码,写论文时章节也自然分得开。

2.4 离线缓存、权限与隐私这些非功能需求别留白

考勤类应用绕不开断网。仓库、地下室、电梯间都可能没信号,用户点了打卡却转圈失败,体验直接崩。做法是本地建一张待提交表,用 Room 存打卡记录和请求 ID,网络恢复后按 ID 去重补传。服务端用requestId做幂等键,重复提交只落一条。

隐私部分有三个必须做到的点:密码只在登录瞬间明文传输一次,服务端加盐哈希存储,客户端不落地;Token 存 EncryptedSharedPreferences,别写进普通 SharedPreferences;日志里打印请求体时过滤掉身份证、手机号、薪资字段。权限申请要跟场景绑定,用户点了「打卡」再申请定位,比启动就弹窗的通过率高得多,也更好写进论文的体验章节。

3. 从 Android Studio 到第一条接口跑通

3.1 安装、汉化与工程目录速览

Android Studio 装完后第一件事是确认 Android SDK 下全了:在 SDK Manager 里勾选对应 API 级别的 Platform、Build-Tools、Platform-Tools 和 Emulator。只装 IDE 不装 SDK,新建工程时会卡在 Gradle 同步。英文界面不习惯的话,在 Settings → Plugins 里搜中文语言包安装并重启,菜单会切成中文;但类名、Gradle 报错、Logcat 依然是英文,排查问题还是得认英文关键词,别把希望全押在汉化上。

工程结构建议按职责分包,而不是按类型堆在一起:ui(Activity/Fragment/Adapter)、data(Repository、Room、本地缓存)、net(Retrofit 接口、拦截器、统一响应体)、model(DTO)、util(时间、定位、加密)。按类型分包到后期会出现一个包下几十个文件,论文里也不好画模块图。

3.2 Gradle 依赖与包分层

依赖集中在模块级构建脚本里,版本号用变量收口,方便统一升级。下面这份是能跑通完整链路的最小集合。

// app/build.gradle android { compileSdk 34 defaultConfig { applicationId "com.example.hrms" minSdk 24 // 覆盖绝大多数在用机型 targetSdk 34 // 决定权限与后台行为 versionCode 1 versionName "1.0" } buildFeatures { viewBinding true } // 干掉 findViewById compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } } dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.recyclerview:recyclerview:1.3.2' 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' annotationProcessor 'androidx.room:room-compiler:2.6.1' implementation 'androidx.security:security-crypto:1.1.0-alpha06' }

minSdk定得越低兼容面越广,但要多写版本分支;targetSdk不要为了省事停在老版本,系统会按旧行为兼容,看着能跑,实际在权限和后台任务上埋雷。viewBinding打开后,布局里的控件通过生成的绑定类访问,比findViewById少一大截空指针。

3.3 Retrofit + OkHttp 拦截器搞定登录态

登录态统一在拦截器里加,别在每个请求里手动塞 Token。同时拦截器还负责识别服务端返回的登录失效,触发一次全局登出。

// net/AuthInterceptor.java public class AuthInterceptor implements Interceptor { private final TokenStore tokenStore; // 内部用 EncryptedSharedPreferences public AuthInterceptor(TokenStore tokenStore) { this.tokenStore = tokenStore; } @Override public Response intercept(Chain chain) throws IOException { Request origin = chain.request(); String token = tokenStore.getToken(); Request request = origin; if (token != null && !token.isEmpty()) { request = origin.newBuilder() .header("Authorization", "Bearer " + token) .header("X-Device-Id", tokenStore.getDeviceId()) // 打卡需要设备标识 .build(); } Response response = chain.proceed(request); if (response.code() == 401) { // 并发请求同时 401 时只登出一次 tokenStore.clear(); EventBus.getDefault().post(new SessionExpiredEvent()); } return response; } }

Authorization用 Bearer 方案,服务端解析头部即可;X-Device-Id是打卡防代打的辅助字段,绑定设备后才能和账号一一对应。401 的处理要注意并发:三个请求同时返回 401,如果每个都触发登出和跳转,会出现连续弹三次登录页,加个标记或事件总线去重就能避免。Retrofit 实例本身用单例持有,OkHttpClient复用连接池,频繁 new 会拖慢首屏。

3.4 Android SDK 与真机联调的报错对照

模拟器跑通不等于真机跑通,尤其是从别的项目移植过来的工程。下面这张表是我踩过最多的几类。

现象常见原因处理方式
Gradle 同步失败、提示 plugin 版本不匹配Gradle Wrapper 与 AGP 版本错配对齐gradle-wrapper.properties与插件版本,别手改单个
应用装上后闪退,日志报 ClassNotFoundmultidex 未开启或依赖冲突开 multidex,用./gradlew app:dependencies查冲突
请求一直失败但浏览器能打开接口明文 HTTP 被拦、或域名未配置全量改 HTTPS,配置 network-security-config
真机连不上电脑但模拟器正常用了localhost10.0.2.2换成局域网 IP,或做一层环境切换
打卡定位永远返回 0.0运行时权限被拒或未开系统定位在回调里区分「权限拒绝」和「定位关闭」两种提示

调试时把 Logcat 过滤成自己的包名,再配 OkHttp 的HttpLoggingInterceptor,只放到 debug 构建里,release 关掉,避免把请求体写进线上日志。

4. 核心模块落地:员工档案、考勤打卡、请假审批

4.1 员工档案列表:RecyclerView 分页与搜索防抖

档案查询是使用频率最高的页面,也是最容易做卡的地方。列表用 RecyclerView + 分页加载,搜索框输入不要每敲一个字就发请求,加 300 毫秒防抖。

// ui/employee/EmployeeListFragment.java(节选) private void onKeywordChanged(String keyword) { searchHandler.removeCallbacksAndMessages(null); searchHandler.postDelayed(() -> { currentPage = 1; viewModel.loadEmployees(keyword, currentPage, 20); // 重新从第一页拉 }, 300); // 防抖窗口,输入停顿后才真正请求 } private void bindLoadMore() { recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView rv, int dx, int dy) { LinearLayoutManager lm = (LinearLayoutManager) rv.getLayoutManager(); if (lm == null || isLoading || !hasMore) return; // 最后一条可见项接近列表末尾时触发下一页 if (lm.findLastVisibleItemPosition() >= adapter.getItemCount() - 3) { viewModel.loadEmployees(currentKeyword, ++currentPage, 20); } } }); }

removeCallbacksAndMessages(null)清掉未执行的旧任务,保证只有最后一次输入生效。分页触发点留 3 个 item 的余量,用户滑到底部时数据已经回来了,看不到空白等待。isLoadinghasMore两个标志位必须成对判断,否则快速滑动会连发多页请求,服务端出现重复数据。

4.2 考勤打卡:时间以服务端为准,定位要留降级

打卡有三个坑:客户端时间可以被改、定位可能拿不到、网络可能断。处理思路是客户端只负责采集,判定全部交给服务端。

// data/AttendanceRepository.java(节选) public void clock(int type, double lng, double lat) { String requestId = UUID.randomUUID().toString(); // 幂等键,重试不重复落库 ClockRequest req = new ClockRequest(type, lng, lat, System.currentTimeMillis(), requestId); api.clock(req).enqueue(new Callback<ApiResponse<ClockResult>>() { @Override public void onResponse(Call<ApiResponse<ClockResult>> call, Response<ApiResponse<ClockResult>> resp) { ClockResult result = resp.body() == null ? null : resp.body().getData(); if (result != null && result.isSuccess()) { cache.remove(requestId); // 成功则清掉本地待补传记录 ui.showClockSuccess(result.getServerTime()); // 展示服务端下发的打卡时间 } else { cache.save(req); // 服务端判定失败,同样入队列等人工处理 } } @Override public void onFailure(Call<ApiResponse<ClockResult>> call, Throwable t) { cache.save(req); // 网络失败落本地,连上网后按 requestId 补传 } }); }

服务端收到clientTime只做参考,判定上班迟到与否用服务器时间,同时把服务端时间回传给客户端展示。这样即使用户改了手机时间,记录也不会错。定位拿不到时不要直接拒绝打卡,降级方案是允许提交并标记为「异常待核」,由 HR 在后台复核,比让员工在门口干等更合理。

4.3 请假审批:状态机与幂等提交

请假单是有生命周期的,用状态字段加枚举控制,别用一堆布尔值拼。典型状态是:待审批、审批中、已通过、已驳回、已撤销。每次流转都要校验当前状态是否允许该动作,否则会出现已驳回的单子还能再通过。

// service/LeaveStateMachine.java public LeaveStatus next(LeaveStatus current, ApproveAction action) { switch (current) { case PENDING: // 待审批:只能通过或驳回 return action == ApproveAction.APPROVE ? LeaveStatus.APPROVED : LeaveStatus.REJECTED; case APPROVED: case REJECTED: // 终态:用户可撤销,撤销后回到已撤销 return action == ApproveAction.CANCEL ? LeaveStatus.CANCELED : current; default: throw new IllegalStateException("非法状态流转: " + current + " -> " + action); } }

提交申请时同样带一个客户端生成的requestId,服务端建唯一索引,网络重试只会命中同一条记录。审批接口要带乐观锁版本号或状态前置条件,主管和 HR 同时点通过时,只有第一个请求生效,第二个返回「状态已变更」,前端刷新列表即可。

4.4 建表 SQL 与索引

表结构不用花哨,关键是字段类型和索引要合理。下面这几张覆盖主链路。

-- 员工表 CREATE TABLE t_employee ( id BIGINT PRIMARY KEY AUTO_INCREMENT, emp_no VARCHAR(20) NOT NULL UNIQUE COMMENT '工号', name VARCHAR(32) NOT NULL, dept_id BIGINT NOT NULL, phone VARCHAR(20), password_hash VARCHAR(128) NOT NULL COMMENT '加盐哈希,绝不存明文', status TINYINT DEFAULT 1 COMMENT '1在职 0离职', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_emp_dept ON t_employee(dept_id); CREATE INDEX idx_emp_name ON t_employee(name); -- 考勤记录表 CREATE TABLE t_attendance ( id BIGINT PRIMARY KEY AUTO_INCREMENT, emp_id BIGINT NOT NULL, clock_type TINYINT NOT NULL COMMENT '1上班 2下班', clock_time DATETIME NOT NULL COMMENT '服务端时间', longitude DECIMAL(10,6), latitude DECIMAL(10,6), request_id VARCHAR(64) NOT NULL COMMENT '幂等键', status TINYINT DEFAULT 1 COMMENT '1正常 2异常待核', UNIQUE KEY uk_request (request_id), KEY idx_emp_time (emp_id, clock_time) );

uk_request这个唯一索引是防重复打卡的关键,比在业务代码里查一遍再插更可靠。idx_emp_time支撑「查某人某月考勤」这个最高频查询。员工姓名用普通索引就够,LIKE '%张%'这种前置模糊匹配走不了索引,数据量上到十万级要考虑全文索引或搜索引擎,毕设规模下不用过度设计。

5. 源码整理、兼容性排查与答辩演示的具体技巧

5.1 源码目录分层与可交付清单

评委翻源码的时间通常只有几分钟,目录一眼看不懂就容易被扣印象分。把 Android 端和服务端放在同一个仓库的两个目录里,根目录留一份 README 说明启动顺序、数据库脚本位置、默认账号。可交付物按这个清单核对:Android 工程(含local.properties模板,不要提交本机 SDK 路径)、服务端工程、schema.sql建表脚本、api.md接口文档、若干张运行截图、环境说明。最容易漏的是数据库初始化脚本,别人拿到源码跑不起来,多半卡在这一步。

版本控制上有个细节值得注意:gradle-wrapper.propertiesbuild.gradle一定要一起提交,只提交其中一个,换台机器同步就失败。真机安装包在build/outputs/apk下,重命名成带版本号和日期的形式,演示当天不会拿错包。

5.2 低端机与高版本 Android 的兼容排查

兼容问题集中在两头:低端机内存小、老系统 API 缺;新系统权限严、后台限制多。低端机上图片列表容易 OOM,把头像加载换成带缓存的图片库并限制尺寸,列表项布局层级压到三层以内。Android 10 以后后台定位受限,如果要做自动打卡,必须明确告知用途并申请后台定位权限,否则进程一退到后台就再也拿不到位置。

Android 版本差异还有几处会直接影响功能:通知渠道从 8.0 起必须建 channel,不建就静默失败;SharedPreferences在 9.0 之后改动不再跨进程可见;12 以后清单里的exported属性必须显式声明,启动就崩基本是这个原因。排查顺序建议是先在模拟器上按 API 级别切换复现,再用真机确认,最后看 Logcat 的完整堆栈,不要只看最后一行。

5.3 演示脚本:三分钟让评审看到闭环

演示不要从头点菜单,按一条完整业务流走:员工登录 → 打卡(展示服务端返回时间)→ 提交请假 → 切到主管账号审批 → 切回员工看状态变成已通过。这条链路把所有角色和状态流转都串起来了,比挨个点页面有说服力。

演练时准备两手兜底。一是断网演示本地队列:关掉网络打卡,界面提示已保存待同步,恢复网络后记录自动补传成功,这一下就把幂等和离线能力讲清楚了。二是准备好接口文档,评审问到「数据从哪来」时直接翻到接口表对应行。数据准备上提前造好一个部门的十来个员工和一个月的考勤记录,列表和统计图才有内容可看,空列表的演示基本等于没演示。最后留一个可复现的细节彩蛋,比如把手机时间改掉再打卡,展示记录时间仍然是服务端时间,这比口头强调「做了防篡改」有力得多。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询