ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot+Android考勤系统实战:B/S架构设计与移动端签到实现

SpringBoot+Android考勤系统实战:B/S架构设计与移动端签到实现 做考勤签到系统这类毕业设计看起来简单实际上很考验你对“移动端服务端”整条链路的理解。项目标题里点名了SpringBoot、Android和B/S架构这三个词基本就决定了系统的基本盘手机端负责打卡和查看记录后台用 Spring Boot 提供接口和数据管理整体业务规则全部收拢在服务端而不是散落在每一个 App 里。如果你正准备做类似选题或者想在企业里搭一套轻量级移动考勤方案这篇内容应该能帮你少踩不少坑。我会按实际开发的顺序来讲先讲为什么选这个架构、再讲数据库和接口怎么设计、然后分别拆后端和 Android 端的实现细节、最后把调试部署过程中遇到的典型问题整理出来。很多内容是常规教程里不会写到的“现场经验”你直接照着做能省下大量试错时间。1. 项目定位与整体设计思路1.1 为什么说“B/S架构”才是移动考勤的正确打开方式很多人一看到 Android 客户端第一反应是纯 C/S 架构客户端里面塞一堆逻辑服务端只做一个简单的数据存取。这个思路放在单机小工具可以放在考勤系统里就会出大问题。考勤系统的核心不是“打卡”这个动作而是“规则”。每个公司的班次不一样、迟到判定标准不一样、外勤处理流程不一样。如果这些规则写在 App 里那么每次调整班次都要发版用户还得去应用商店更新。换成B/S 架构App 只是一个“采集终端”它只负责三件事拿到用户身份、采集定位/时间/设备信息、把数据送回服务端。至于这个打卡是正常、迟到还是外勤全部由 Spring Boot 服务端计算判定。这样做的好处非常直接改规则不用重新打包 App改服务端逻辑即可即时生效。同一个服务端可以同时支撑 Android App、微信小程序、Web 管理后台。数据、日志、报表都集中在服务端方便审计和二次分析。我当时做架构图的时候没有画得很复杂就是用一张分层图表达最上层是 Android 客户端和浏览器管理端中间是 Spring Boot 的 REST API 层下面挂 MySQL 存储业务数据、Redis 做缓存和防重复提交再往下是文件存储和定位服务。这个结构既符合毕设的演示需求也基本贴近真实产品的部署形态。1.2 技术栈选型背后的取舍先说后端。Spring Boot几乎没什么悬念社区成熟、招人门槛低、资料多。但版本选择有讲究。如果你用的是 JDK 8老老实实选 Spring Boot 2.7.x 系列别追新。Spring Boot 3.x 虽然出来很久了但它要求 JDK 17并且把javax.*换成了jakarta.*包名很多老教程里的代码直接复制过来会编译不过。对毕业设计来说踩版本坑是非常不划算的时间开销。持久层我推荐 MyBatis-Plus而不是原生 MyBatis。原生 MyBatis 写分页、写条件查询比较啰嗦MyBatis-Plus 提供BaseMapper单表 CRUD 基本不用写 SQL可以节省大量时间。如果你更习惯 Spring Data JPA也可以但考勤系统里存在大量自定义统计查询MyBatis-Plus 的 SQL 可控性更好。权限认证方面用JWT不用 Session。原因是移动端没有浏览器那种天然的 Cookie 容器用 Session 要处理会话保持、跨域携带 Cookie很麻烦。JWT 的无状态特性更适合这种 App API 的组合Token 放请求头里服务端一验就能拿到用户身份水平扩展也容易。具体技术栈可以看这张表层次选型选型理由后端框架Spring Boot 2.7.x兼容 JDK 8资料多稳定ORMMyBatis-Plus单表零 SQL统计查询可控数据库MySQL 5.7 / 8.0免费、通用、面试常问缓存Redis存 Token、防重复提交、缓存班次规则认证JWT无状态适合移动端接口鉴权Android 网络层Retrofit OkHttp使用广泛拦截器方便加 TokenAndroid 本地存储Room用于离线打卡数据暂存Android 架构ViewModel LiveData轻量足够应付考勤场景Android 端我没有引入太重的组件化框架。一个考勤 App 的页面量通常不大登录页、打卡主页、记录页、个人中心再加一个管理员视图。用 Kotlin ViewModel LiveData 就够了不要为了“显得高级”去上 Compose 或者 Hilt 这类东西毕业设计最重要的是完整跑通和逻辑清晰。2. 核心功能拆分与数据库设计2.1 功能模块怎么拆才不会做乱考勤系统最怕做成“一个大而全的怪物”。我当时拆模块的思路是从一条完整的打卡闭环倒推的员工打开 App 打卡服务端记录时间和地点后台管理员查看汇总报表人事处理请假补卡。按这条线拆成四个模块员工端打卡上班签到、下班签退、外勤打卡、查看我的考勤记录。考勤规则管理班次设置固定班/排班、迟到早退阈值、打卡有效半径。审批与异常处理请假申请、补卡申请、管理员审批。统计报表按月汇总出勤天数、迟到次数、早退次数、缺卡记录可导出 Excel。模块之间不要交叉调用。比如员工端打卡时只需要去查“考勤规则”和“班次”不需要直接操作“审批”表。接口按模块对应成若干 Controller逻辑清楚后面写论文也好展开。2.2 六张核心表就够了别过度设计我见过不少同学一上来设计十几张表什么“打卡照片表”“设备绑定表”“操作日志表”结果业务还没写就先把关系搞晕了。实际上考勤系统最核心的表只有六张employee员工表department部门表shift班次表attendance_record打卡记录表leave_application请假申请表attendance_rule考勤规则表其中最重要的就是attendance_record它是整个系统的数据中枢。我第一次设计这张表时只想着存user_id和check_time两个字段后来发现完全不够用。真正跑起来之后你需要回答这些临时问题这个员工是在哪打的卡用的哪台设备打的卡打卡时的定位偏移多少是否在外勤范围如果表里没有冗余字段这些问题全部要回查日志。因此打卡表最终长这样CREATE TABLE attendance_record ( id BIGINT NOT NULL AUTO_INCREMENT, employee_id BIGINT NOT NULL COMMENT 员工ID, department_id BIGINT NULL COMMENT 部门ID冗余存储, attendance_date DATE NOT NULL COMMENT 考勤日期, check_in_time DATETIME NULL COMMENT 上班签到时间, check_out_time DATETIME NULL COMMENT 下班签退时间, check_in_lat DECIMAL(10, 6) NULL COMMENT 签到纬度, check_in_lng DECIMAL(10, 6) NULL COMMENT 签到经度, check_in_address VARCHAR(255) NULL COMMENT 签到位置描述, check_out_lat DECIMAL(10, 6) NULL COMMENT 签退纬度, check_out_lng DECIMAL(10, 6) NULL COMMENT 签退经度, check_out_address VARCHAR(255) NULL COMMENT 签退位置描述, device_id VARCHAR(64) NULL COMMENT 设备标识, client_trace_id VARCHAR(64) NOT NULL COMMENT 客户端生成的唯一ID防重复提交, status TINYINT NOT NULL DEFAULT 0 COMMENT 0正常 1迟到 2早退 3外勤 4缺卡, remark VARCHAR(255) NULL COMMENT 备注, PRIMARY KEY (id), UNIQUE KEY uk_employee_date (employee_id, attendance_date), KEY idx_department (department_id, attendance_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考勤打卡记录表;关键点我给你标出来了attendance_date单独用 DATE 类型而不是直接拿时间字段做日期比较否则 SQL 里到处是DATE_FORMAT、DATE()函数索引全部失效。client_trace_id是客户端生成的一个 UUID每次打卡请求都不同用来做重复提交拦截这是我自己踩过坑之后补上的字段后面会细讲。经纬度和地址一定要存快照因为员工离开打卡点后你不能再依赖当前定位去反推当时的打卡位置。2.3 接口设计规范一眼看懂是做什么的接口这块最忌讳的是像写桌面程序一样把所有操作揉进一个/api/doSomething。我采用 RESTful 风格资源名都用名词复数动作交给 HTTP 方法表达方法路径功能权限POST/api/auth/login登录获取 Token匿名POST/api/attendance/check-in上班签到员工POST/api/attendance/check-out下班签退员工POST/api/attendance/outside外勤打卡员工GET/api/attendance/my?month2025-06查看个人月度打卡记录员工GET/api/attendance/export?month2025-06导出考勤报表管理员POST/api/leave提交请假申请员工PUT/api/leave/{id}/approve审批请假管理员GET/api/rule/current查询当前生效的考勤规则员工这里有一个重要细节打卡记录和考勤月报不能做成同一个接口。打卡记录返回的是明细可能一个月有几十条数据月报返回的是汇总统计比如出勤天数、迟到次数。如果混在一起前端要么接一堆用不到的字段要么每个页面都要重新算一遍。接口职责单一前后端配合起来才顺畅。3. 后端Spring Boot 实现的关键代码与原理解析3.1 springboot项目结构按照业务分层不要“一包走天下”很多初学的朋友喜欢把 Controller、Service、Mapper 全塞在同一个包下这在小 demo 里没问题但做到考勤系统这种规模代码量会上来找文件就会非常痛苦。我习惯的分层结构是这样的com.example.attendance ├── config // 配置类跨域、拦截器、Swagger、Redis ├── controller // 接口层 ├── service // 业务逻辑 ├── mapper // MyBatis-Plus 的 Mapper 接口 ├── entity // 数据库实体 ├── dto // 请求/响应DTO比如打卡DTO、登录DTO ├── common // 统一返回结果、异常处理、常量 └── utils // JWT工具、距离计算工具、日期工具Controller 层只做参数接收和结果包装业务逻辑全在 Service 层。比如打卡这个动作Controller 里就一行调用attendanceService.checkIn(dto)具体判定周期、校验位置、计算状态都放在 Service 里。这样写有两个好处一是单元测试时不需要启动 Web 容器二是如果以后加一个 Web 打卡入口可以直接复用 Service不用再复制逻辑。3.2 关键的 springboot配置JWT 拦截器与跨域先看application.yml的核心配置别把端口、时区这些细节忽略了server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/attendance?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 mybatis-plus: mapper-locations: classpath:/mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl jwt: secret: your-secret-key-change-me expire-days: 7serverTimezoneAsia/Shanghai一定要加。MySQL 驱动 8.x 版本对时区敏感不加会在连接时报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized纯纯的中文编码时区乱码问题。JWT 的secret不要写在代码里放到配置文件里方便不同环境切换。然后是拦截器配置。这里有个新手常踩的坑Spring Boot 默认用的是 Spring MVC但如果你在类上标注了Configuration并且实现了WebMvcConfigurer要注意 Spring Boot 2.6 之后默认会使用CGLIB 代理。这个默认行为其实影响很小但当你在拦截器里注入UserService的时候如果代理方式不对可能出现循环依赖。我的经验是拦截器里的类依赖尽量用构造器注入不要用字段Autowired。拦截器核心逻辑很简单就是校验请求头里的 TokenComponent public class JwtInterceptor implements HandlerInterceptor { private final JwtUtils jwtUtils; private final RedisTemplateString, String redisTemplate; public JwtInterceptor(JwtUtils jwtUtils, RedisTemplateString, String redisTemplate) { this.jwtUtils jwtUtils; this.redisTemplate redisTemplate; } Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); Long employeeId jwtUtils.parseToken(token); if (employeeId ! null) { request.setAttribute(employeeId, employeeId); return true; } } response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\未登录或登录已过期\}); return false; } }注意我同时接了 Redis 来判断 Token 是否被强制退出。JWT 本身是无状态的但如果员工改密码或者管理员想踢人下线光靠 JWT 做不到需要把 Token 的 jti唯一标识存在 Redis 里退出时删除拦截时查一下。这个细节在毕设答辩时非常加分面试官也会另眼相看。3.3 考勤打卡的业务逻辑一个接口解决签到、迟到判断、定位校验打卡接口是整个后端最核心的地方。我一开始把逻辑想简单了以为就是往表里插一条记录后来发现要处理的问题远比想象中多同一员工一天只能打一次上班卡、必须判断打卡时间是否在班次范围内、必须判断位置是否在有效半径内、客户端有可能重复请求。所以我把打卡 Service 做成了“流水线”Service public class AttendanceServiceImpl implements AttendanceService { Autowired private AttendanceRecordMapper attendanceRecordMapper; Autowired private ShiftMapper shiftMapper; Autowired private AttendanceRuleMapper ruleMapper; Autowired private RedisTemplateString, String redisTemplate; Override public AttendanceRecord checkIn(CheckInDTO dto, Long employeeId) { LocalDate today LocalDate.now(); // 1. 防重复提交基于 Redis 分布式锁key attendance:checkin:{employeeId}:{today} String lockKey attendance:checkin: employeeId : today; Boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, 1, Duration.ofSeconds(10)); if (Boolean.FALSE.equals(locked)) { throw new BusinessException(请求太频繁请勿重复打卡); } try { // 2. 查询当日是否已有打卡记录 AttendanceRecord record attendanceRecordMapper.selectOne( new LambdaQueryWrapperAttendanceRecord() .eq(AttendanceRecord::getEmployeeId, employeeId) .eq(AttendanceRecord::getAttendanceDate, today)); if (record ! null record.getCheckInTime() ! null) { throw new BusinessException(今日已签到请勿重复操作); } // 3. 查询班次规则判断迟到 Shift shift shiftMapper.selectCurrentShift(employeeId, today); LocalTime shiftStart shift.getStartTime(); LocalTime shiftLateThreshold shift.getLateThreshold(); // 例如 09:15 int status 0; LocalTime now LocalTime.now(); if (now.isAfter(shiftLateThreshold)) { status 1; // 迟到 } // 4. 校验打卡位置 AttendanceRule rule ruleMapper.selectCurrentRule(); double distance GeoUtils.distance(dto.getLatitude(), dto.getLongitude(), rule.getOfficeLat(), rule.getOfficeLng()); if (distance rule.getValidRadiusMeters()) { status 3; // 外勤 } // 5. 插入记录 if (record null) { record new AttendanceRecord(); record.setEmployeeId(employeeId); record.setAttendanceDate(today); record.setCheckInTime(LocalDateTime.now()); record.setCheckInLat(dto.getLatitude()); record.setCheckInLng(dto.getLongitude()); record.setCheckInAddress(dto.getAddress()); record.setClientTraceId(dto.getClientTraceId()); record.setDeviceId(dto.getDeviceId()); record.setStatus(status); attendanceRecordMapper.insert(record); } return record; } finally { redisTemplate.delete(lockKey); } } }注意我在 Redis 锁这一层设置了一个 10 秒的过期时间防止极端情况下进程崩溃导致锁永远不释放。数据库层面attendance_record表里有uk_employee_date唯一索引双重保险。3.4 经纬度距离计算不能忽略地球曲率这里补充一个很多人容易犯的错误计算打卡距离时直接用平面直角坐标系公式也就是Math.sqrt((lat1-lat2)^2 (lng1-lng2)^2)。如果两个点相距很近误差还能忍但一旦涉及几百米到几公里的范围这个公式在纬度越高的地方误差越离谱因为经度方向上的 1 度在不同纬度下对应距离完全不同。正确做法是使用 Haversine 公式它假设地球是一个半径约 6371 公里的球体计算球面上两点间的大圆距离。实现也不复杂public class GeoUtils { private static final double EARTH_RADIUS 6371.0; // 公里 public static double distance(double lat1, double lng1, double lat2, double lng2) { double radLat1 Math.toRadians(lat1); double radLat2 Math.toRadians(lat2); double dLat Math.toRadians(lat2 - lat1); double dLng Math.toRadians(lng2 - lng1); double a Math.sin(dLat / 2) * Math.sin(dLat / 2) Math.cos(radLat1) * Math.cos(radLat2) * Math.sin(dLng / 2) * Math.sin(dLng / 2); double c 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return EARTH_RADIUS * c * 1000; // 返回米 } }在实际测试的时候我建议用高德地图或者 Google Maps 的测距工具先研究一下“打卡点”到“公司坐标”的实际距离再对比接口算出来的值。如果偏差超过 50 米不要急着改代码先检查 Android 端传上来的坐标是 GCJ-02 还是 WGS-84。国内地图 SDK 默认返回的是 GCJ-02 加密后的坐标而后端如果按 WGS-84 的公司坐标去算会差几百米非常坑。4. Android 端移动打卡客户端的落地细节4.1 权限申请与 Android 版本适配Android 端的权限问题是新手最容易崩溃的地方。考勤 App 至少需要定位权限如果要做扫码签到还需要相机权限。Android 6.0 开始运行时权限需要动态申请Android 11 调整了包可见性规则Android 13 新增了通知权限POST_NOTIFICATIONS。如果你的打卡成功需要推送通知那这个权限也得申请。我直接用了XXPermissions这个开源库它的好处是帮你在底层处理兼容判断。核心代码非常简短XXPermissions.with(this) .permission(Permission.ACCESS_FINE_LOCATION) .permission(Permission.CAMERA) .permission(Permission.POST_NOTIFICATIONS) .request { permissions, allGranted - if (allGranted) { startLocation() } else { showToast(缺少必要权限部分功能不可用) } }这里我想强调一个设计上的点不要在 App 启动第一时间就弹权限框。很多应用一启动就要定位权限用户会觉得很冒犯直接拒绝。更自然的做法是进入“打卡”页面时用户主动点击“签到”按钮再弹权限框。这样权限的用途非常明确用户的授权意愿也高。4.2 网络层封装Retrofit 拦截器统一加 TokenAndroid 端的网络层我选 Retrofit OkHttp没有什么悬念。重点在于把 Token 的附加逻辑放到 OkHttp 拦截器里不要在每个请求方法里手动传Authorization头否则代码里会到处重复。class TokenInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val originalRequest chain.request() val token SharedPrefsUtil.getToken() val newRequest if (token.isNullOrEmpty()) { originalRequest } else { originalRequest.newBuilder() .header(Authorization, Bearer $token) .build() } return chain.proceed(newRequest) } }同时要在 OkHttp 里统一处理 401 响应。我见过的初级代码每个接口回调里都要判断一次 code 是不是 401非常繁琐。正确做法是在拦截器里看到 401就发一个事件通知页面去重新登录。用 LiveData 或 EventBus 都可以实现核心是“全局统一处理”不散落在各业务代码里。4.3 打卡页面的定位获取与状态展示定位是 Android 端最不稳定的部分。室内定位精度差、GPS 半天拿不到经纬度、用户关闭了定位服务这些问题都可能导致打卡失败。我的做法是进入打卡页时先启动定位页面上用一个 loading 状态展示“正在定位...”。如果 3 秒内拿到高精度定位显示具体地址和“可打卡”状态如果超时使用最近一次的缓存定位并提示“定位耗时较长位置可能不准确”。定位成功后把经纬度和地址封装成 DTO点击打卡按钮时一并提交。参考实现是这样class MainViewModel : ViewModel() { private val _locationState MutableLiveDataLocationUiState() val locationState: LiveDataLocationUiState _locationState fun refreshLocation() { _locationState.value LocationUiState.Loading locationClient.lastLocation .addOnSuccessListener { location - if (location ! null) { _locationState.value LocationUiState.Success( lat location.latitude, lng location.longitude, address geocoder(location.latitude, location.longitude) ) } else { _locationState.value LocationUiState.Failed(定位失败请检查GPS或网络) } } .addOnFailureListener { e - _locationState.value LocationUiState.Failed(e.message ?: 定位失败) } } }这里有一个反直觉的注意点手机 GPS 拿到的坐标是 WGS-84 标准但高德地图 SDK 拿到的坐标是 GCJ-02。如果你的后端用的是高德坐标前端直接传 GPS 原生坐标会有偏差。要么统一用高德 SDK 获取坐标要么后端做坐标转换。最保险的方法是Android 端直接用高德定位 SDK把定位结果的原始坐标当作“公司坐标体系”后端计算时也用同一套坐标。4.4 离线模式下的补卡设计真实场景里员工在电梯、地下车库、地铁里网络不好是常态。打卡按钮点了之后请求失败用户会再点一次这时候非常容易出现重复数据。我在项目里做了离线处理用户点击打卡数据先写入 Room 本地库同时显示“等待上传”状态网络恢复后由 WorkManager 后台任务统一上传上传成功后更新本地状态。每条离线记录必须有一个client_trace_id通常是 UUID。服务端根据这个字段做幂等判断同一个 traceId 只处理一次。这样哪怕离线队列里同一业务被触发两次服务端也不会生成两条考勤记录。如果不做这一层用户在地铁里多点了几次打卡后台就会出现一天有三条签到记录非常尴尬。5. 调试与部署中踩过的坑5.1 springboot版本太高引发的连锁问题我看到太多人直接创建 Spring Boot 3.x 项目然后发现整合 MyBatis-Plus 时要么版本不对、要么javax.annotation不存在、要么 CGLIB 代理的配置类报错。做考勤系统这种毕设级项目真没必要追新。我最后固定使用 Spring Boot 2.7.18JDK 8MyBatis-Plus 3.5.3 版本组合整整开发期间没有出现一次依赖冲突。有一类常见的报错是spring-boot-starter-web自带的 Tomcat 版本过高导致在低版本 JDK 环境启动报UnsupportedClassVersionError。这通常不是你代码的问题而是项目用了 JDK 17 编译配置运行环境却是 JDK 8。在 IDEA 里检查Project Structure - SDK和Java Compiler两个地方必须一致。5.2 Android 9 限制 HTTP 明文流量如果你的后端服务器用的是 HTTP 而不是 HTTPSAndroid 9API 28之后默认禁止明文流量所有请求会直接抛Cleartext traffic not permitted。这个问题在开发阶段特别容易遇到因为本地调试时后端通常是http://10.0.2.2:8080或者局域网 IP。解决办法是在AndroidManifest.xml里给application标签加android:usesCleartextTraffictrue或者使用网络安全配置文件指定允许明文的域名application android:usesCleartextTraffictrue ... 这不是一个推荐用于生产环境的配置但做毕设演示完全够用。如果以后要上线直接上 HTTPS这个配置就可以去掉。5.3 Android Studio 的中文设置与模拟器调试关于 Android Studio 界面语言的问题新版本在安装时可以选择中文语言包也可以在Settings - Plugins里搜索 Chinese Language Pack。但我的个人建议是用英文界面遇到问题搜资料时更容易对上号。你搜“怎么设置中文”不如直接搜英文关键词“change language android studio”来得快。这不是看不看得懂的问题而是开发资料大多是英文环境习惯了界面英文查 Stack Overflow 时不容易懵。模拟器调试定位时你可以在 AVD 的 Extended Controls扩展控制里手动发送一个经纬度坐标给模拟器这样就能模拟在公司附近的打卡。这个功能在开发阶段非常实用不然你坐在实验室里永远测不出“距公司 300 米外打卡是否为外勤”的效果。5.4 时区问题与“改手机时间作弊”考勤系统最实用的一个防作弊手段是后端不用客户端传的时间而是自己取服务器时间。如果客户端把当前时间传上来那员工改一下手机系统时间就能伪造打卡或者把手机时区改到东十二区打卡记录直接飞到明天。因此打卡接口设计时我特意不看dto.getTime()服务端一律使用LocalDateTime.now()。数据库连接串也必须带serverTimezoneAsia/Shanghai否则 MySQL 默认会话时区可能是 UTC存进去的时间比你本地时间晚 8 个小时。这个问题如果你用 Navicat 看数据白天打的卡显示凌晨时间那就是时区配错了。5.5 并发打卡与 Redis 锁的边界前面代码里用了 RedissetIfAbsent做防重复提交但没有用 Lua 脚本释放锁严格来说是有隐患的如果业务执行时间超过锁过期时间锁会自动释放此时第二个请求进来拿到了锁第一个请求结束删除了第二个请求的锁。这个在并发极小、单机部署的毕设项目里几乎不会发生但如果你要写到简历里建议用SET lock_key uuid EX 10 NX释放时校验值是否一致// 释放锁时确保是自己的锁 String value redisTemplate.opsForValue().get(lockKey); if (uuid.equals(value)) { redisTemplate.delete(lockKey); }另外数据库的唯一索引uk_employee_date是最终的兜底防线。Redis 锁可能因为宕机、网络问题失效但数据库唯一约束是绝对可靠的。我建议两套方案都做缺一不可。6. 部署上线与后续扩展建议6.1 Maven 打包与后端部署开发环境跑通之后部署其实很简单。后端打包用 Maven执行mvn clean package -DskipTests然后会在target目录下生成一个可执行 Jar用java -jar启动就可以。生产环境我不建议直接前台运行最好用 nohup 放到后台nohup java -jar attendance-server.jar --spring.profiles.activeprod app.log 21 如果有 Nginx 做反向代理记得把/api/前缀转发到 Java 服务的 8080 端口并且配置 WebSocket 的超时时间。Android 端的BASE_URL要改成服务器公网地址不要写localhost否则手机访问的是手机自己。6.2 管理后台要不要单独部署把 Vue 打进 Spring Boot很多人的毕设要求里有一个 Web 管理端有些同学选择把 Vue 项目单独部署结果要同时维护两个服务。实际上可以直接用 Spring Boot 把前端构建产物给“吞”掉Vue 项目执行npm run build后把生成的dist目录里的所有文件复制到 Spring Boot 的src/main/resources/static目录下重新打包。这样整个系统只有一个 Jar既省服务器资源又不用处理跨域问题。同一个域名下浏览器直接访问管理页面接口走同源路径复杂度瞬间降下来。6.3 从毕设到落地还有哪些扩展点如果这个项目不只是为了让老师通过而是想真正应用到企业或者写进简历下面这几个点值得延伸动态考勤组不同部门适用不同班次而不是全公司一个班次。请假与打卡联动请假审核通过后当天不生成缺卡记录。企业微信/钉钉通知打卡成功、迟到提醒、请假审批结果通过消息推送触达员工。报表导出用 EasyExcel 导出月考勤汇总表方便人事直接存档。人脸识别二次验证防止员工“代打卡”可以在打卡时要求活体检测。我个人在实际操作中的一个经验是先不要急着优化 UI而是把“打卡 → 数据入库 → 后台看到汇总”这条主链路跑通再回头丰富细节。考勤系统最难的不是界面漂不漂亮而是数据准不准、重复不重复、边界清不清楚。只要把这几件事抓稳这个项目的完成度就超过大多数同类毕业设计了。最后再分享一个小技巧安卓端调试时别每次都用真机连着数据线直接用adb reverse tcp:8080 tcp:8080让手机访问http://localhost:8080就能调到电脑上的后端服务。你会发现开发效率瞬间提升不少。项目后续如果还想加 WebSocket 实时通知、加多级审批、加智能排班这套架构都不用推翻重来按模块往里面加就是了。
返回列表