尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

考勤系统全栈实现:Android客户端、Spring Boot服务端与数据库设计详解

考勤系统全栈实现:Android客户端、Spring Boot服务端与数据库设计详解 简介基于Android的考勤系统是一套完整的毕业设计/课程设计参考案例面向Java与Android开发者解决企业考勤管理中的移动打卡、请假审批、考勤统计与数据同步等核心问题。压缩包包含1016个文件总计约19.92MB涵盖Android客户端源码、jsp与class构成的服务端实现、SQL数据库脚本、xml配置、jar依赖库以及png/gif/jpg界面素材可支撑从客户端到服务端再到数据库的全链路学习与二次开发。已有775人学习浏览适合需要完整项目源码进行课程设计、论文写作或实际项目参考的开发者。它不仅提供可运行的apk安装包还展示了GPS定位打卡、Retrofit网络通信、MVC架构、JWT鉴权、SQLite本地缓存、Spring Boot接口开发等关键知识点数据库部分包含用户表、考勤表、请假表和角色权限表设计并附pdf说明文档便于直接运行验证与逐模块剖析。1. 考勤系统为什么值得拆开看客户端与服务端的边界在哪里每天早高峰的地铁上总有人用手机完成一次打卡。这个动作背后Android 端要做定位、发请求、写缓存服务端要做鉴权、判迟到、落库数据库要保证记录不丢不乱。一套考勤系统正好把 Android 开发、后端接口和关系型数据库串在同一条业务线上。这个压缩包里是客户端源码、服务端源码和数据库脚本适合做毕业设计、课程设计或想搞懂移动端与服务端如何联调的开发者。它不是只有登录页的 Demo而是把打卡、请假、记录查询这些真实流程完整串起来解压后就是 Android Studio 工程、Spring Boot 工程和 SQL 脚本。我拆这类项目有个习惯先不看代码先把“一次打卡请求从手机到数据库要走哪几步”画出来。这一步想清楚后面读三端源码都会快很多。2. Android 客户端Retrofit 网络层、GPS 定位与本地缓存的配合2.1 客户端界面模块与职责划分这个项目的 Android 端按功能可以切成四个界面模块登录、打卡、请假、记录查询。登录页拿到 token 后存入 SharedPreferences之后所有接口请求都靠这个 token 做身份标识打卡页是核心展示当前时间和位置点击按钮触发定位与网络提交按钮在请求期间要切换成加载进度条并禁用防止重复提交请假页是一张表单提交后由管理员在服务端侧审批记录页用 RecyclerView 渲染历史打卡列表。如果源码里 Activity 写得比较重后续可以考虑把逻辑抽到 ViewModel 中让界面和业务解耦。界面核心职责关键技术点登录页用户名密码校验保存 tokenSharedPreferences、POST 请求打卡页定位、提交打卡、防重复点击LocationManager、Retrofit、ProgressBar请假页表单校验、提交请假申请表单校验、日期选择记录页展示考勤历史与状态RecyclerView、下拉刷新UI 层之后最值得看的是网络层。毕业设计里最常见的写法是直接用 HttpURLConnection 裸写请求而合格的工程化写法是用 Retrofit 封装接口。下面这段是考勤场景下惯用的接口定义方式。public interface AttendanceApi { POST(api/attendance/checkin) CallApiResponseCheckinResult checkin(Body CheckinRequest request); GET(api/attendance/records) CallApiResponseListAttendanceRecord getRecords(Query(yearMonth) String yearMonth); }POST和GET分别声明 HTTP 方法和相对路径Body会把 CheckinRequest 序列化成 JSON 放到请求体里Query则是把 yearMonth 拼到 URL 后面。Retrofit 配合 GsonConverterFactory返回的 JSON 会自动映射成 ApiResponse 泛型对象省掉了手写 JSON 解析的重复劳动。需要注意网络请求不能放在主线程Retrofit 的enqueue回调本身是异步的回调里要更新 UI 时再切回主线程这是 Android 开发者最容易犯的线程错误。2.2 OkHttp 拦截器为每个请求附加认证信息Retrofit 底层默认使用 OkHttp 做网络传输所以给所有请求统一加 Header 的常用做法是写一个拦截器。token 在登录之后存到本地拦截器里每次请求自动带上 Authorization 头服务端就能识别出是哪个用户在调用接口不需要在每一个接口调用处手动传 token。OkHttpClient client new OkHttpClient.Builder() .addInterceptor(chain - { Request original chain.request(); String token sessionManager.getToken(); Request request original.newBuilder() .header(Authorization, Bearer token) .method(original.method(), original.body()) .build(); return chain.proceed(request); }) .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build();connectTimeout控制建立连接的超时时间readTimeout控制读取响应的超时时间。在早高峰地铁这类弱网场景下这两个值如果设得太短打卡请求很容易失败设得太长用户会一直卡在进度条状态。10 秒是一个比较折中的起调值。拦截器拿到的 token 如果为空说明登录态已失效一般会配合response.code() 401的统一判断来触发重新登录。2.3 GPS 定位获取经纬度和防代打卡的边界打卡页的核心是拿到当前经纬度。最常见的实现是直接使用 LocationManager 获取最后已知位置生产级方案则建议使用 FusedLocationProvider它能融合 GPS、Wi-Fi 和基站信号给出更稳定的结果。课程设计里的简化版多数是 LocationManager 双 Provider 回退。LocationManager locationManager (LocationManager) getSystemService(LOCATION_SERVICE); if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ! PackageManager.PERMISSION_GRANTED) { return; } Location location locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); if (location null) { location locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); } if (location ! null) { checkinRequest.setLatitude(location.getLatitude()); checkinRequest.setLongitude(location.getLongitude()); }这里有两个容易被忽视的点。第一getLastKnownLocation拿到的可能是几分钟前的缓存位置所以更严谨的流程是先requestLocationUpdates拿到最新值再取回调里的 location第二仅靠客户端上报经纬度并不能真正防止代打卡因为位置可以被模拟。服务端还必须校验每次打卡的时空合理性比如打卡地点与公司坐标的距离超过 500 米就标记为异常让管理员人工处理。这也是我常说“防作弊要靠服务端规则”的原因。2.4 用 Room 缓存未提交的打卡记录考勤场景对网络波动极其敏感早高峰电梯里信号差是常事。一个可行的方案是把“待提交”的打卡记录先写进本地 SQLite 或 Room等网络恢复后由后台任务重试提交这样不会因为一次网络超时让员工漏打卡。Entity(tableName pending_checkin) public class PendingCheckin { PrimaryKey(autoGenerate true) public long id; public double latitude; public double longitude; public long timestamp; public int retryCount; }retryCount字段用来记录重试次数避免网络永远不通时无限重试常见的策略是重试超过 5 次后标记为失败并通知用户手动处理。这里有一个必须说明的取舍缓存写入成功但服务端实际未收到的情况下客户端显示的“打卡成功”是乐观的所以在 UI 上应该区分“已提交”和“待同步”两种状态而不是让用户误以为已经完成考勤。要把打卡体验做好这条本地缓存链路和主流程一样值得测试。3. 服务端Spring Boot 接口、JWT 认证与数据安全的落地方案3.1 工程分层与接口清单服务端是典型的 Spring Boot 工程按 Controller、Service、Mapper 三层划分。Controller 只负责接收参数和返回结果业务规则全部下沉到 Service数据访问交给 Mapper。项目里主要的接口可以归纳成下面这张表登录接口不鉴权其他接口全部走 JWT 拦截器方法路径说明鉴权POST/api/auth/login登录并返回 JWT不需要POST/api/attendance/checkin提交打卡JWTGET/api/attendance/records查询考勤记录JWTPOST/api/leave/apply提交请假申请JWTGET/api/leave/approve审批请假管理员管理员和普通员工的差异体现在请假审批和考勤报表查询上这个差异由用户角色字段控制而不是给每个接口单独写权限判断。阅读源码时先把这个接口清单和数据库表对应上整条链路的轮廓就清晰了。3.2 登录认证与 JWT 令牌设计登录接口做的事情很明确根据用户名查出用户用 BCrypt 比对密码匹配成功后生成 JWT 返回给客户端。密码绝不能明文存储BCrypt 是 Spring Security 里默认支持的哈希算法每次哈希结果带随机盐同样的密码两次哈希结果不同能有效抵抗彩虹表攻击。PostMapping(/api/auth/login) public ApiResponseLoginResult login(RequestBody Valid LoginRequest req) { User user userService.findByUsername(req.getUsername()); if (user null || !passwordEncoder.matches(req.getPassword(), user.getPassword())) { throw new BizException(用户名或密码错误); } String token jwtUtil.generateToken(user.getId(), user.getUsername(), user.getRole()); return ApiResponse.ok(new LoginResult(token)); }passwordEncoder.matches接收的第二个参数是从数据库读出的哈希值第一个参数是用户输入内部会自动校验盐和哈希是否匹配。jwtUtil.generateToken负责把用户 id、用户名、角色写进 JWT 的 payload并用服务端私钥签名。客户端后续请求只要带上这个 token拦截器验签通过后就能从 token 里解析出用户身份。看源码时建议重点看两点一是 token 的过期时间二是退出登录后 token 如何失效。常见的做法是设置 24 小时过期再配合客户端删除本地 token 来实现登出。3.3 打卡接口的业务校验打卡接口比想象中要复杂因为它不仅要写一条数据还涉及重复打卡判断、上班时间窗判断、迟到早退状态计算。Controller 层保持轻量把业务逻辑放到 Service 里是保证后续好维护的关键。PostMapping(/api/attendance/checkin) public ApiResponseCheckinResult checkin(RequestBody Valid CheckinRequest req) { Attendance attendance attendanceService.doCheckin( currentUserId(), req.getLatitude(), req.getLongitude(), req.getTime()); return ApiResponse.ok(CheckinResult.of(attendance)); }currentUserId()是从 JWT 拦截器放入的上下文里取的不能信任客户端传过来的 userId否则伪造请求就能替别人打卡。Service 内部至少要完成三件事检查当天是否已经打过卡、根据打卡时间计算状态、把经纬度和状态写入 attendance 表。判断重复打卡时要注意时区问题数据库和服务器时间要用同一时区否则跨天时很容易误判。源码里如果用了LocalDateTime.now()要留意服务器默认时区是否已经正确设置。3.4 统一异常处理与参数校验服务端接口数量一多异常处理就会变得很乱。最省事的做法是用全局异常处理器兜底业务异常抛出 BizException参数校验失败的异常单独处理这样接口里就不用到处写 try-catch。RestControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(BizException.class) public ApiResponseVoid handleBiz(BizException e) { return ApiResponse.error(e.getCode(), e.getMessage()); } ExceptionHandler(MethodArgumentNotValidException.class) public ApiResponseVoid handleValid(MethodArgumentNotValidException e) { String message e.getBindingResult().getFieldErrors().stream() .map(f - f.getField() f.getDefaultMessage()) .collect(Collectors.joining(; )); return ApiResponse.error(400, message); } }参数校验由Valid触发比如 CheckinRequest 里的 latitude 字段标上NotNull(message 纬度不能为空)请求缺字段时就会抛 MethodArgumentNotValidException。这里把错误信息按“字段 提示语”拼接返回客户端能直接弹提示。统一错误码的设计也很重要比如 401 代表未登录403 代表无权限客户端拿到后可以统一跳转登录页而不是每个接口各自处理一套返回结构。4. 数据库设计用户表、考勤表、请假表如何支撑完整的打卡闭环4.1 四张核心表的建模思路数据库脚本是这个项目里最实在的部分。它的四张核心表对应了考勤业务的主干用户表存员工信息考勤表存打卡流水请假表存请假申请角色权限表控制功能可见性。表设计上遵循第三范式业务字段拆分到不同表里避免重复存储。实际项目中角色权限表往往简化成用户表里的一个 role 字段考勤系统的权限粒度不需要做到菜单级源码里常见的也是这种轻量方案。拿到 SQL 脚本后用 Navicat 或 mysql 命令行导入都可以注意执行前要先创建好数据库并选择 utf8mb4 字符集否则中文字段和备注容易出现乱码。4.2 建表语句与关键字段解析用户表的主键用自增 idusername 加唯一索引。password 字段长度给 64正好容纳 BCrypt 哈希输出。department 和 role 字段支撑部门考勤统计和权限判断。CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(32) NOT NULL, password VARCHAR(64) NOT NULL, real_name VARCHAR(32) NOT NULL, department VARCHAR(64) DEFAULT , role TINYINT NOT NULL DEFAULT 1 COMMENT 1-员工 2-管理员, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;考勤表是流水表每条记录对应一次打卡。latitude 和 longitude 用 DECIMAL(10,6)精度足够到米级。status 用 TINYINT 而不是字符串是为了查询和索引更高效。user_id 和 checkin_time 的联合索引是查询历史记录和高频过滤的基石。CREATE TABLE attendance ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, checkin_time DATETIME NOT NULL, latitude DECIMAL(10,6) DEFAULT NULL, longitude DECIMAL(10,6) DEFAULT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-正常 1-迟到 2-早退, remark VARCHAR(255) DEFAULT , PRIMARY KEY (id), KEY idx_user_checkin (user_id, checkin_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段类型说明user_idINT关联 user 表checkin_timeDATETIME打卡时间latitude / longitudeDECIMAL(10,6)打卡位置statusTINYINT考勤状态remarkVARCHAR备注比如外出办事外键约束在考勤流水表里可以不用因为打卡记录是高频写入外键会影响插入性能。保持 user_id 和 user 表的逻辑关联查询时通过 JOIN 取员工姓名和部门是这类业务表更常见的做法。4.3 一次打卡数据是怎么完成落库的把一次打卡的数据流转串起来看客户端拿到经纬度后组装 CheckinRequest服务端 JWT 拦截器解析出 userIdService 判断当天没有重复记录后执行 INSERT最后把自增 id 和状态返回给客户端。这里的重复打卡判断和 INSERT 必须放在同一个事务里否则两个请求同时进来时可能出现双写。Spring Boot 里只要在 Service 方法上标注Transactional就能保证这一点。请假表的设计思路类似只不过多了 start_time、end_time 和审批状态字段。管理员审批时 UPDATE 状态员工端下拉刷新就能看到结果。这里有一个设计细节值得注意请假通过后考勤统计时要排除请假时间段否则员工明明请了假报表里却显示旷工。源码里如果考虑到了这一点说明数据库设计就不是停留在建表层面而是真正理解了业务闭环。4.4 考勤统计的典型查询考勤报表是管理端最常用的功能。一个月的出勤统计可以用一条分组聚合 SQL 完成这也是数据库增删改查之外最该掌握的聚合查询场景。SELECT user_id, COUNT(*) AS total_days, COUNT(CASE WHEN status 0 THEN 1 END) AS normal_days, COUNT(CASE WHEN status 1 THEN 1 END) AS late_days FROM attendance WHERE user_id 1001 AND checkin_time BETWEEN 2025-01-01 AND 2025-01-31 GROUP BY user_id;COUNT 配合 CASE WHEN 的写法比 SUM(IF(...)) 可读性好也更容易扩展新状态。如果表数据量超过几十万行BETWEEN 会走 idx_user_checkin 联合索引性能影响不大。但报表查询如果涉及到部门维度建议在 SQL 里先 JOIN user 表取 department 字段再做分组而不要在应用层循环统计。部门数据量大时可以再给 user 表的 department 加一个普通索引避免全表扫描。5. 把源码跑起来环境配置、联调验证与常见坑位排查5.1 Android 9 以上访问 HTTP 明文接口的两个办法服务端在本地跑的时候通常没有 HTTPS 证书而 targetSdk 28 以上的 Android 默认禁用了明文 HTTP 流量。最常见的解决方案是在 AndroidManifest.xml 里临时开启明文流量。application android:usesCleartextTraffictrue ...更规范的写法是配置 networkSecurityConfig只允许开发环境的域名走明文。比如 baseUrl 填的是http://10.0.2.2:8080就在 network_security_config.xml 里放行这个域名。真机调试时把地址换成电脑的局域网 IP模拟器则固定用 10.0.2.2 访问宿主机。这个报错在 Android Studio 里表现为请求直接失败且没有任何响应体抓包也看不到请求发出遇到这种情况先检查明文配置。5.2 模拟器定位与真机联调的两个验证动作考勤功能依赖定位模拟器上默认位置经常是空的。可以用 adb 给模拟器打一个经纬度坐标。adb emu geo fix 116.397128 39.916527执行后模拟器的 GPS 数据会被更新到指定坐标这时再打开打卡页就能拿到有效经纬度。真机调试时要注意 Android 6.0 以上必须动态申请 ACCESS_FINE_LOCATION 权限而且要在 onRequestPermissionsResult 里处理用户拒绝的情况否则点打卡只会静默失败。定位权限的申请时机建议放在进入打卡页时而不是 App 启动时用户的授权意愿会更高。5.3 用 curl 快速验证服务端接口客户端还没调通之前先用 curl 把服务端接口验证一遍能省下很多联调时间。登录拿 token 再打卡是完整链路的最小验证。curl -X POST http://localhost:8080/api/auth/login \ -H Content-Type: application/json \ -d {username:admin,password:123456}拿到返回的 token 后再发起打卡请求服务端返回 200 就说明接口链路是通的。之后再看 Android 端的代码问题范围就缩小到客户端网络配置或权限上了。我通常会再抓一次包确认 Authorization 头确实带上排除掉拦截器没生效的情况这套排查顺序基本能覆盖联调阶段的绝大多数问题。本文还有配套的精品资源点击获取
返回列表