简介基于安卓的人事管理系统设计与实现毕业设计资料包面向计算机相关专业学生、安卓开发者和需要完成毕业设计课题的读者。内容围绕企业人事管理场景完整覆盖从需求分析、可行性分析、系统设计、数据库建模到登录模块、员工管理、部门管理、职位管理、休假管理等核心功能的实现同时阐述Java语言、MySQL数据库、JSP技术及安卓开发工具的应用能够帮助读者掌握移动管理类项目的完整开发流程。压缩包内含一个Word文档包体大小约1.11MB文档集成论文正文、系统分析设计说明与测试内容适合按照目录逐章阅读也可为后续二次开发或论文扩展提供参照。目前已有187人学习下载。读者可从中学到技术选型思路、模块划分方法、数据库设计要点和测试策略对完成毕业设计答辩、补充项目文档或研发同类人事管理系统具有实用价值。1. 从纸质考勤到一个移动端人事系统的距离一个百人规模的某公司HR 每月初要花两个工作日核对考勤、算薪资、催审批单。即使上了某款 PC 端人事软件管理者出差在外时审批和花名册查询依然回不了邮件就得拖半天。这就是基于 Android 的人事管理系统设计与实现(论文源码)这类项目存在的核心理由把人事管理里最高频的查询、打卡、审批、通讯录场景搬到手机上让员工和管理者各自在自己的终端上完成闭环操作。这类方向最近热度一直不低很多从业者把它当作移动端全栈练手的首选——因为它不像电商那样重业务、重并发表结构清晰权限模型简单却覆盖了登录鉴权、网络层封装、原生相机与定位、通知推送、离线缓存这些最常见的移动端硬技能。系统通常包含两个端员工用的 Android 客户端和管理端 Web 后台中间走 JSON 接口。本文以 Android 客户端为主视角给出一个能跑通全流程的落地路径架构选型、数据库设计、核心功能实现、以及五个绕不开的坑。不论你是拿它做毕业设计还是给某公司内部做一套轻量移动办公工具照着这套思路走能少走很多弯路。2. 技术选型与整体架构先把地基打稳2.1 为什么客户端选原生 Android 而不是跨平台框架市面上 Flutter、React Native 之类的跨平台方案很成熟但这类人事管理系统里原生 Android 依然是更稳的选择。原因不只是就业市场需求侧重的差异而是业务本身的需求侧考勤打卡需要调用系统级定位和相机、审批需要快速集成签名面板、通知栏消息需要精确控制通知渠道 —— 这些能力在原生环境里踩坑成本最低生态最成熟。如果某一天要同时出 iOS 版服务端接口已经 JSON 化客户端几乎不用动后端。架构模式上MVP 或 MVVM 都合适。小型人事系统不建议上重型组件化方案一个单模块工程配合 MVVM 足够清晰。ViewModel LiveData Repository 这套组合Google 官方维护学习资料多遇到生命周期问题社区里一键可查。我一般会在 build.gradle 里配置 viewModel 和 liveData 的依赖数据层单独抽一个 Repository 类Activity 只做视图渲染和事件分发不直接写 SQL 或调网络。2.2 服务端接口和本地存储的落地选择服务端这部分常见做法是 Spring Boot 或者 Node.js 的 Express按 REST 风格暴露接口。客户端这一侧网络层用 Retrofit OkHttp这是 Android 开发的事实标准。Retrofit 负责把接口定义成 Java 接口方法OkHttp 负责底层连接和拦截器回调统一走 RxJava 或者协程。考虑到人事系统的接口量不大用协程比 RxJava 的学习成本低代码也直观。implementation com.squareup.retrofit2:retrofit:2.9.0 implementation com.squareup.retrofit2:converter-gson:2.9.0 implementation com.squareup.okhttp3:logging-interceptor:4.9.3 implementation org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4依赖选版本时注意Retrofit 2.9.0 是 2.x 系列的最终版稳定Gson converter 和 Retrofit 版本要配套否则会抛不兼容异常。协程的 Android 扩展库已经并入主库不需要额外引入 kotlinx-coroutines-android 之外的组件。本地存储方面需要区分两类数据一类是结构化的用户信息和审批记录用 Room另一类是临时性的 Token 和登录态用 SharedPreferences 或者 DataStore。Room 是 SQLite 的官方抽象层编译期校验 SQL省去手写游标转换的繁琐代码。Entity(tableName employee) data class Employee( PrimaryKey val empId: String, val name: String, val department: String, val position: String, val phone: String, val email: String, val avatarUrl: String? )这张表对应员工花名册的本地缓存。注意 PrimaryKey 用了 empId 字符串而不是自增 id因为服务端的员工编号是业务主键本地自增 id 会造成两端数据对应混乱。avatarUrl 可空因为有些员工可能没上传头像。缓存这类数据的目的是让员工在无网络环境下也能快速查到同事电话这在出差或开会时体验差异极大。3. 功能模块与数据库设计把业务装进表里3.1 人事系统必备功能清单与权限边界一个完整可交付的人事系统功能上至少覆盖员工自助个人信息、工资条、考勤打卡、请假/加班审批、公告通知、部门通讯录、管理端的人员管理和审批流处理。权限分三种角色普通员工、部门主管、HR 管理员。每条接口请求都带上角色信息服务端做鉴权不能只靠客户端隐藏按钮来限制功能。功能模块员工端动作管理端动作核心数据表考勤打卡上班/下班打卡、查看月历查看考勤统计attendance请假审批提交申请、查看进度通过/驳回leave_request通讯录按部门浏览、搜索维护组织架构department、employee公告通知查收、标记已读发布/撤回announce工资查询查看工资条导入/维护工资数据salary数据库不要只为一个角色设计。比如 employee 表里要预留 department 字段permission 表要能支撑角色变化。现实中常见翻车是只做了员工端管理端留个空壳结果项目结项时被要求补全部管理功能改动量等于重做。3.2 核心表的建表 SQL 与字段设计数据库设计决定整个系统的维护成本。这里直接给出精简可用的核心表结构-- 员工表 CREATE TABLE employee ( emp_id VARCHAR(20) PRIMARY KEY, name VARCHAR(50) NOT NULL, department_id INT NOT NULL, position VARCHAR(50), phone VARCHAR(20), email VARCHAR(100), hire_date DATE, status TINYINT DEFAULT 1 ); -- 考勤表 CREATE TABLE attendance ( id INT AUTO_INCREMENT PRIMARY KEY, emp_id VARCHAR(20) NOT NULL, work_date DATE NOT NULL, check_in_time DATETIME, check_out_time DATETIME, status VARCHAR(10) DEFAULT normal, UNIQUE KEY uk_emp_date (emp_id, work_date) ); -- 请假申请表 CREATE TABLE leave_request ( id INT AUTO_INCREMENT PRIMARY KEY, emp_id VARCHAR(20) NOT NULL, leave_type VARCHAR(20) NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, reason TEXT, approver_id VARCHAR(20), status VARCHAR(10) DEFAULT pending, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );这三张表是整个系统的骨架。考勤表上加了 (emp_id, work_date) 的唯一索引防止同一天重复打卡记录。leave_request 的 approver_id 存的是审批人不一定等于部门主管因为有些公司特批流程要绕过直属上级。字段类型要提前想清楚时间一律用 DATETIME 存完整时间戳不要拆成日期和时分两个字段否则后面做跨天班次统计时会非常痛苦。请假时长不要入表用 start_time 和 end_time 计算得出避免冗余字段在不同时区下出现不一致。3.3 建表之后的事务与索引设计人事系统并发量不高但事务还是要写对。以请假审批为例审批通过后不仅要更新 leave_request 的状态还要往 attendance 表插入一条请假记录或者标记为 leave这两个动作必须在一个事务里否则会出现审批已通过但考勤里没有记录的脏数据。Transactional public void approveLeave(Integer leaveId, String approverId) { LeaveRequest leave leaveMapper.selectById(leaveId); leave.setStatus(approved); leave.setApproverId(approverId); leaveMapper.update(leave); // 插入考勤异常记录避免漏打卡误判 attendanceMapper.insertLeaveRecord( leave.getEmpId(), leave.getStartTime(), leave.getEndTime()); }注意先更新申请状态、再插入考勤记录的顺序。如果先插入考勤再更新状态网络异常时会出现考勤有请假记录但审批单还是 pending 的情况前端展示就会矛盾。索引设计也不用贪多。employee 表的 department_id 要加索引因为通讯录按部门浏览是高频操作leave_request 的 emp_id 和 status 联合索引要加因为员工查自己的历史审批单是最常用入口。其他字段不加索引多了会拖慢写入速度。4. Android 客户端核心实现从登录到打卡的落地代码4.1 登录鉴权和 Token 管理不能用明文口令登录是客户端和服务端的第一次握手。正确流程是客户端把账号密码用 HTTPS POST 提交给 /api/auth/login服务端校验后返回一个 TokenJWT 或 Opaque Token客户端存到本地后续所有请求在 Header 里带 Authorization: Bearer 。Token 有过期时间客户端要响应 401 并跳回登录页。interface AuthApi { POST(api/auth/login) suspend fun login(Body request: LoginRequest): ApiResponseLoginResult } class AuthRepository(private val api: AuthApi) { suspend fun login(username: String, password: String): LoginResult? { val response api.login(LoginRequest(username, password)) return if (response.code 0) response.data else null } }登录接口的密码传输客户端不需要自己做 MD5 或 RSA 加密再上传HTTPS 已经保证了传输层安全。很多人在这层画蛇添足本地再用 MD5 加密一次反而导致服务端没法用 BCrypt 做密码校验。Token 存储用 SharedPreferences 还是 DataStore小项目 SharedPreferences 够用但注意不要存明文可以用 Android Keystore 加密后存储。有的人会把 token 直接存在全局静态变量里App 一杀进程就丢用户每次打开都要重新登录体验不好。正确做法是App 启动时从 SharedPreferences 读取 token内存里也留一份两者配合。4.2 网络层封装统一处理超时、重试和 401没有统一封装的网络层后期每个页面都要处理 loading、错误提示和 token 过期代码会爆炸。下面是一个基于 OkHttp 拦截器实现的 token 自动附加和 401 自动登出class AuthInterceptor(private val tokenProvider: () - String?) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val token tokenProvider() val request if (token.isNullOrEmpty()) { chain.request() } else { chain.request().newBuilder() .header(Authorization, Bearer $token) .build() } val response chain.proceed(request) if (response.code 401) { // 通知上层 token 失效跳登录页 } return response } }拦截器里不直接弹 Toast 或跳页面因为拦截器工作在工作线程UI 操作要发消息回主线程。常见做法是定义 一个全局事件总线或者 LiveData401 到达时发送一个事件由 BaseActivity 统一接收处理。OkHttp 的超时参数默认值偏保守要在构建 OkHttpClient 时显式设置connectTimeout 10 秒、readTimeout 15 秒、writeTimeout 15 秒。连接超时太短用户在公司弱 WiFi 环境下会频繁失败太长接口异常时用户要等很久才能看到重试按钮。4.3 考勤打卡定位、拍照与时间容错考勤打卡是人事系统里客户端最重头的功能。基础流程分三段定位校验、拍照留存、提交时间戳。定位校验的目的是确认员工在办公范围内不能把打卡做成纯靠信任的系统。private fun doCheckIn() { val location getLastKnownLocation() if (location null) { showToast(无法获取定位请检查GPS是否开启) return } val distance calculateDistance( location.latitude, location.longitude, OFFICE_LAT, OFFICE_LNG ) if (distance 500) { showDialog(当前不在考勤范围内是否继续打卡) return } takePhotoAndUpload { photoUrl - submitAttendance(photoUrl, System.currentTimeMillis()) } }这里有一个很关键的细节拍照和提交不是一个动作。先拍照上传拿到 URL再把 URL 和时间戳一起提交给服务端。如果反过来先把打卡记录入库再传照片照片传失败时记录里就缺了凭证管理员根本没法核对拍照打卡是不是本人。定位获取要用 LocationManager 的 GPS_PROVIDER 和 NETWORK_PROVIDER 双 Provider 配合。国内手机 GPS 冷启动可能要几十秒纯等定位回调用户会等崩溃。我一般会用 LocationClient 设置 3 秒轮询拿到一次有效定位就立即使用避免打卡页面卡在定位中。4.4 通讯录和审批列表从本地缓存读取的正确姿势通讯录页面的数据量级通常是几百到几千人远端接口一次拉全量是合理的但不要每次进入页面都请求网络。正确做法是首次进入时拉取全量并写入 Room 缓存后续加载优先读缓存同时后台静默刷新。这样即使断网通讯录功能依然可用。class ContactRepository(private val dao: EmployeeDao, private val api: EmployeeApi) { suspend fun getDepartmentContacts(deptId: Int): ListEmployee { val cached dao.getByDepartment(deptId) if (cached.isNotEmpty()) { return cached } val remote api.fetchByDepartment(deptId) dao.insertAll(remote) return remote } }注意这个读缓存策略的取舍有缓存就直接返回没有才请求网络。它的缺点是缓存可能不是最新数据比如某员工今天入职他在旧缓存里不存在。所以通讯录页面要做一个下拉刷新入口用户主动刷新时强制走网络并更新 Room 缓存。审批列表同理但多一个状态筛选。用户查看待审批列表时如果审批流的状态在另一端发生了变化本地缓存会显示陈旧数据。我的处理习惯是待审批列表强制走网络已办列表读缓存因为已办数据不会频繁变动而待审批是强实时场景。4.5 今日打卡状态的本地判断与当日判重打卡按钮的可用状态要结合服务端返回的今日打卡记录和本地时间判断。很多系统把判断逻辑写死在服务端但 App 在弱网状态下请求慢用户等不及会连点按钮Service 就要做幂等处理。val today SimpleDateFormat(yyyy-MM-dd, Locale.getDefault()).format(Date()) viewModel.todayRecord.observe(this) { record - binding.btnCheckIn.isEnabled record?.checkOutTime null if (record?.checkOutTime ! null) { binding.tvStatus.text 今日已完成上下班打卡 } }这里有个边界场景要注意跨天。如果公司有夜班或者加班到凌晨的员工他前一天晚上 10 点打了下班卡第二天早上的今日打卡判断不能只按自然日算要和班次绑定。这个需求如果产品原型里没有明确服务端最好预留 shift_id 字段避免后期扩展时改表结构。5. 移动端人事系统避坑清单5 条血泪经验5.1 坑一定位权限申请不当导致打卡功能整体不可用现象部分 Android 10 及以上机型打卡页面打开后一直显示定位失败或者点击按钮无响应。原因Android 6.0 开始动态权限已经分成普通权限和危险权限定位属于危险权限。到 Android 10API 29又增加了后台定位权限如果 App 用了前台服务做定位但没有申请 ACCESS_BACKGROUND_LOCATION系统会直接拒绝。更隐蔽的是很多国产 ROM 默认把精确定位开关关掉只给了粗略定位权限。解决运行时同时申请 ACCESS_FINE_LOCATION 和 ACCESS_COARSE_LOCATION并且在 Manifest 里声明用到的定位能力。在打卡页面首次进入时主动检查权限不要等用户点按钮才弹窗。国产 ROM 还需要引导用户到设置中打开定位服务单靠权限请求弹窗是不够的。5.2 坑二服务端时间与手机本地时间不一致导致打卡记录错位现象员工打卡时间在服务端显示提前或延后 1 小时甚至出现在错误的日期。原因客户端把 System.currentTimeMillis() 直接作为打卡时间传给了服务端。员工手机时区设置错误、时间自动同步关闭或者出差时没有切时区都会导致提交的时间戳与服务端实际时间不符。解决客户端只上传原始的时间戳并同时上传 timezoneOffset 字段服务端以自己接收请求的时间为准加上时区偏移量计算员工本地时间。打卡记录以服务端时间为主客户端时间只做展示。这条规则要写进接口文档里前后端各保留一份否则后期联调会非常痛苦。5.3 坑三审批流程状态不同步导致员工重复提交现象员工提交请假申请后页面卡在审批中实际主管已通过但员工端一直没刷新。员工以为没提交成功重新提交了一遍服务端出现两条相同申请。原因审批列表读缓存时间太长且提交接口没有做幂等校验。解决提交请假接口增加一个 clientRequestIdUUID服务端用唯一索引或 Redis 做幂等判断同一个 clientRequestId 只能提交一次。客户端在提交成功后本地记录该 ID如果网络超时重试时带上同一个 ID服务端直接返回已有的审批结果不会产生重复单。5.4 坑四数据库连接池配置不当导致考勤高峰期服务端卡死现象月末考勤核对期间员工集中打卡服务端响应时间从 200ms 涨到 8 秒部分请求直接超时。原因数据源的连接池最大连接数设成了默认的 10而打卡接口同时会查询员工表和考勤表一个请求可能要占用 2 个连接。高峰期并发 30 个请求就把连接池打满了后续请求全部排队。解决按并发量评估连接池参数。这个小系统的业务体量下initialSize5、maxActive50 是比较稳妥的配置。另外把打卡接口里的 SQL 仔细审核避免在事务里做全表扫描考勤表按 emp_id work_date 建好索引后单次查询必须走索引。5.5 坑五图片上传失败导致考勤记录不完整现象员工完成打卡后管理员后台看到考勤记录存在但没有打卡照片。出错概率不大但每个月总有那么几条。原因客户端打卡流程先提交记录再上传图片上传失败时没有重试机制也没有提示用户。解决把图片上传挪到打卡记录提交之前前文已提到的方案并增加重试队列。如果上传失败打卡按钮置灰并提示照片上传失败请检查网络后重试不要让用户带着残缺数据打卡成功。6. 进阶技巧离线数据同步与消息触达策略6.1 离线打卡的补偿机制移动办公场景下电梯里、地下车库里没有信号是常态。打卡功能要做离线补偿客户端在本地生成一条 pending 状态的打卡记录包含时间戳和拍摄的照片本地路径等网络恢复后自动同步到服务端。实现思路是 Room 里建一张 sync_queue 表记录待同步的数据类型、数据 JSON 和重试次数。App 启动时和网络状态变化时触发同步成功后删除记录失败则累计重试次数超过 5 次就不再自动重试改为在打卡页面给出提示。这里有一个坑要注意离线打卡的照片本地路径在 App 进程被杀后可能失效如果照片存放在缓存目录系统清理缓存会把它删掉。所以离线照片要放到外部存储的应用专属目录不要用 cache 目录。6.2 审批通知本地轮询还是推送审批通知是人事系统体验的关键点。很多开发者用推送 SDK 做但这类系统通常没有专职运维服务端证书配置复杂还可能因为推送通道在国内手机后台被杀而丢消息。我一般建议用 WorkManager 做定时轮询每 30 分钟查一次待审批数量有变化时发一条本地通知。实现成本低稳定性高不依赖任何第三方服务。轮询的接口设计成返回待审批数量即可不要每次拉全量列表流量和电量消耗都可控。6.3 最后说点实在话这个系统的坑我都踩过一遍。当年某次上线就是栽在时间戳上——出差员工在另一个时区打了卡记录全部错位最后对数据对到深夜。从那以后我的所有接口文档第一行永远是所有时间以服务端为准。希望这篇笔记能帮你把路铺平一点你动手做的时候记得把权限和时区这两个最容易翻车的地方优先处理好。希望帮到你。本文还有配套的精品资源点击获取