Spring Boot + uni-app 智慧考勤闭环 Demo:打卡记录、异常状态和日统计如何复用到企业系统

发布时间:2026/7/5 1:36:24

Spring Boot + uni-app 智慧考勤闭环 Demo:打卡记录、异常状态和日统计如何复用到企业系统 这篇不讲“企业系统应该数字化”这种空话直接从智慧考勤项目里抽一条可以复用的工程链路移动端采集打卡数据后端按规则落库PC 后台查询和修正最后沉淀成日统计。真实项目里这一条链路分布在几个位置后端实体zhkq-api/jeecg-module-zhkq/src/main/java/org/jeecg/modules/biz/entity/KqAttendanceRecord.java移动端接口zhkq-uniapp/src/common/http/api.js移动端打卡页zhkq-uniapp/src/pages/wqdk/wqgw.vueApp 控制器AppKqAttendanceRecordController后端服务IKqAttendanceRecordService、KqAttendanceRecordServiceImplMapperKqAttendanceRecordMapper、KqAttendanceRecordMapper.xmlPC 后台接口zhkq-web/src/views/attendanceRecord/clockIn/clock.in.api.ts这套设计能复用到巡检、运维、门店拜访、工单签到、设备保养、上门服务等系统。原因很简单这些业务都不是只要一条“提交记录”而是要解决“现场数据可信、异常可解释、后台可追溯、统计可回算”。下面给一个脱敏后的完整源码 Demo。代码不是内部项目原文件全文复制而是参照现有工程的字段、分层、接口风格和业务边界整理出来的最小闭环。一、为什么考勤记录不能只存一张流水表智慧考勤里的 KqAttendanceRecord 不是简单的“某人几点打了卡”。真实字段至少要覆盖五类信息字段类型核心字段作用人员组织personId、personName、unitId、departmentId支持租户、单位、部门维度查询打卡类型clockType、upDownWorkClock区分单位打卡、外勤打卡、紧急外勤、上班、下班证据数据clockTime、longitude、latitude、clockAddress、clockImg形成位置、时间、图片证据链规则关联attendanceRule、attendanceWork保留当时命中的规则和班次后续规则变更不影响历史解释流程状态clockStatus、afStatus、leaveRecordId、createType支持异常、申诉、请假、定时任务补记录如果只存 person_id clock_time后面所有问题都会变成查聊天记录员工说定位不准主管说超出范围HR 说月底统计对不上老板说报表不可信。二、完整源码 Demo考勤打卡闭环1. Entity考勤记录表package com.demo.attendance.entity; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.time.LocalDateTime; /** * 考勤记录实体。 * 负责保存员工一次打卡的结果和证据业务边界是“事件记录”不直接承担规则配置和月度统计职责。 */ Data TableName(demo_attendance_record) public class AttendanceRecord { /** 主键 */ private String id; /** 员工ID */ private String personId; /** 员工姓名脱敏 Demo 中只保存展示名 */ private String personName; /** 单位ID */ private String unitId; /** 部门ID */ private String departmentId; /** 打卡类型1单位打卡2外勤打卡3紧急外勤 */ private String clockType; /** 上下班类型1上班2下班 */ private String upDownWorkClock; /** 打卡状态0正常1迟到2缺卡3早退4旷工5请假6补卡 */ private Integer clockStatus; /** 申诉状态0正常1申诉中3驳回 */ private Integer appealStatus; /** 打卡时间 */ private LocalDateTime clockTime; /** 打卡地址 */ private String clockAddress; /** 经度 */ private String longitude; /** 纬度 */ private String latitude; /** 打卡照片多个文件用逗号分隔 */ private String clockImg; /** 外勤或异常说明 */ private String attendanceContent; /** 命中的考勤规则ID */ private String attendanceRule; /** 命中的班次ID */ private String attendanceWork; /** 创建方式1员工打卡2定时任务补记录 */ private Integer createType; /** 软删除标识0正常1删除 */ private Integer delFlag; }2. DTO移动端打卡入参package com.demo.attendance.dto; import lombok.Data; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import java.time.LocalDateTime; /** * 移动端打卡入参。 * 只接收客户端可以提供的现场证据人员、组织和最终状态由后端根据登录态和规则补齐。 */ Data public class ClockInDTO { /** 上下班类型1上班2下班 */ NotBlank(message 上下班类型不能为空) private String upDownWorkClock; /** 打卡类型1单位打卡2外勤打卡3紧急外勤 */ NotBlank(message 打卡类型不能为空) private String clockType; /** 客户端采集的打卡时间 */ NotNull(message 打卡时间不能为空) private LocalDateTime clockTime; /** 经度 */ private String longitude; /** 纬度 */ private String latitude; /** 客户端解析到的地址缺失时后端兜底解析 */ private String clockAddress; /** 打卡照片 */ private String clockImg; /** 外勤说明 */ private String attendanceContent; /** 移动端命中的规则ID */ private String ruleId; /** 移动端命中的班次ID */ private String workId; }3. VOPC 后台列表返回package com.demo.attendance.vo; import lombok.Data; import java.time.LocalDateTime; /** * 考勤记录展示对象。 * 用于 PC 后台列表和移动端详情避免把内部控制字段直接暴露给页面。 */ Data public class AttendanceRecordVO { private String id; private String personName; private String departmentName; private String clockTypeName; private String upDownWorkClockName; private String clockStatusName; private Integer appealStatus; private LocalDateTime clockTime; private String clockAddress; private String clockImg; private String attendanceContent; }4. MapperMyBatis Plus 基础查询 日记录查询package com.demo.attendance.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.demo.attendance.entity.AttendanceRecord; import com.demo.attendance.vo.AttendanceRecordVO; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.time.LocalDate; import java.util.List; /** * 考勤记录 Mapper。 * 负责记录表的基础 CRUD 和面向页面的组合查询不处理规则判断。 */ Mapper public interface AttendanceRecordMapper extends BaseMapperAttendanceRecord { /** * 查询某员工某天的打卡记录。 * * param personId 员工ID * param date 日期 * return 当日记录 */ ListAttendanceRecordVO selectDayRecord(Param(personId) String personId, Param(date) LocalDate date); }对应 XML?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.demo.attendance.mapper.AttendanceRecordMapper select idselectDayRecord resultTypecom.demo.attendance.vo.AttendanceRecordVO SELECT id, person_name AS personName, department_id AS departmentName, CASE clock_type WHEN 1 THEN 单位打卡 WHEN 2 THEN 外勤打卡 WHEN 3 THEN 紧急外勤 ELSE 未知 END AS clockTypeName, CASE up_down_work_clock WHEN 1 THEN 上班打卡 WHEN 2 THEN 下班打卡 ELSE 未知 END AS upDownWorkClockName, CASE clock_status WHEN 0 THEN 正常 WHEN 1 THEN 迟到 WHEN 2 THEN 缺卡 WHEN 3 THEN 早退 WHEN 4 THEN 旷工 WHEN 5 THEN 请假 WHEN 6 THEN 补卡 ELSE 异常 END AS clockStatusName, appeal_status AS appealStatus, clock_time AS clockTime, clock_address AS clockAddress, clock_img AS clockImg, attendance_content AS attendanceContent FROM demo_attendance_record WHERE person_id #{personId} AND del_flag 0 AND DATE(clock_time) #{date} ORDER BY clock_time ASC /select /mapper5. Service接口定义package com.demo.attendance.service; import com.baomidou.mybatisplus.extension.service.IService; import com.demo.attendance.dto.ClockInDTO; import com.demo.attendance.entity.AttendanceRecord; import com.demo.attendance.vo.AttendanceRecordVO; import java.time.LocalDate; import java.util.List; /** * 考勤记录业务服务。 * 负责打卡落库、重复打卡控制、状态判断和日统计触发。 */ public interface AttendanceRecordService extends IServiceAttendanceRecord { /** * 员工打卡。 * * param dto 移动端打卡数据 * param loginUserId 当前登录员工ID * return 结果提示 */ String clock(ClockInDTO dto, String loginUserId); /** * 查询某天考勤记录。 * * param personId 员工ID * param date 日期 * return 当日打卡记录 */ ListAttendanceRecordVO dayRecord(String personId, LocalDate date); }6. ServiceImpl状态判断、时间防篡改和日统计更新package com.demo.attendance.service.impl; import cn.hutool.core.util.IdUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.demo.attendance.dto.ClockInDTO; import com.demo.attendance.entity.AttendanceRecord; import com.demo.attendance.mapper.AttendanceRecordMapper; import com.demo.attendance.service.AttendanceRecordService; import com.demo.attendance.service.AttendanceStatsService; import com.demo.attendance.vo.AttendanceRecordVO; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; /** * 考勤记录业务实现。 * 从真实项目抽取了三个关键约束客户端时间不可信、同一班次不能重复覆盖、记录落库后要同步日统计。 */ Slf4j Service RequiredArgsConstructor public class AttendanceRecordServiceImpl extends ServiceImplAttendanceRecordMapper, AttendanceRecord implements AttendanceRecordService { private final AttendanceStatsService attendanceStatsService; Override Transactional(rollbackFor Exception.class) public String clock(ClockInDTO dto, String loginUserId) { LocalDateTime now LocalDateTime.now(); // 防止手机系统时间被调到未来。真实项目中超过当前时间 10 分钟会拒绝。 if (dto.getClockTime().isAfter(now.plusMinutes(10))) { throw new IllegalArgumentException(打卡失败请先校准手机系统时间); } // 防止补很久以前的卡。真实项目中超过 90 天会重置为当前时间。 if (dto.getClockTime().isBefore(now.minusDays(90))) { dto.setClockTime(now); } LocalDate clockDate dto.getClockTime().toLocalDate(); boolean duplicated this.count(new LambdaQueryWrapperAttendanceRecord() .eq(AttendanceRecord::getPersonId, loginUserId) .eq(AttendanceRecord::getAttendanceWork, dto.getWorkId()) .eq(AttendanceRecord::getUpDownWorkClock, dto.getUpDownWorkClock()) .apply(DATE(clock_time) {0}, clockDate)) 0; if (duplicated) { throw new IllegalStateException(当前班次已存在打卡记录请勿重复提交); } AttendanceRecord record new AttendanceRecord(); record.setId(IdUtil.fastSimpleUUID()); record.setPersonId(loginUserId); record.setPersonName(演示员工); record.setUnitId(demo-unit); record.setDepartmentId(demo-dept); record.setClockType(dto.getClockType()); record.setUpDownWorkClock(dto.getUpDownWorkClock()); record.setClockTime(dto.getClockTime()); record.setLongitude(dto.getLongitude()); record.setLatitude(dto.getLatitude()); record.setClockAddress(resolveAddress(dto)); record.setClockImg(dto.getClockImg()); record.setAttendanceContent(dto.getAttendanceContent()); record.setAttendanceRule(dto.getRuleId()); record.setAttendanceWork(dto.getWorkId()); record.setClockStatus(matchClockStatus(dto)); record.setAppealStatus(0); record.setCreateType(1); record.setDelFlag(0); this.save(record); attendanceStatsService.rebuildDayStats(loginUserId, clockDate); return 打卡成功; } Override public ListAttendanceRecordVO dayRecord(String personId, LocalDate date) { return baseMapper.selectDayRecord(personId, date); } /** * 地址兜底。 * 真实项目中如果前端未传地址会用经纬度反查地址Demo 中只保留结构。 */ private String resolveAddress(ClockInDTO dto) { if (dto.getClockAddress() ! null !dto.getClockAddress().trim().isEmpty()) { return dto.getClockAddress(); } if (dto.getLongitude() ! null dto.getLatitude() ! null) { return 经纬度解析地址; } return 未知位置; } /** * 判断打卡状态。 * 真实项目会结合班次时间、延迟分钟、请假、外勤审批等规则Demo 只保留可复用骨架。 */ private Integer matchClockStatus(ClockInDTO dto) { if (2.equals(dto.getClockType()) dto.getAttendanceContent() null) { return 4; } return 0; } }7. 日统计 Service记录和结果分层package com.demo.attendance.service; import java.time.LocalDate; /** * 考勤日统计服务。 * 负责把多条打卡记录汇总为当天结果避免列表页每次重新计算。 */ public interface AttendanceStatsService { /** * 重算某员工某天统计。 * * param personId 员工ID * param date 日期 */ void rebuildDayStats(String personId, LocalDate date); }package com.demo.attendance.service.impl; import com.demo.attendance.service.AttendanceStatsService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.time.LocalDate; /** * 考勤日统计实现。 * Demo 只展示触发位置真实项目可在这里统计迟到、早退、缺卡、外勤、请假和补卡次数。 */ Slf4j Service public class AttendanceStatsServiceImpl implements AttendanceStatsService { Override public void rebuildDayStats(String personId, LocalDate date) { log.info(重算考勤日统计 personId{}, date{}, personId, date); } }8. Controller移动端提交和查询package com.demo.attendance.controller; import com.demo.attendance.dto.ClockInDTO; import com.demo.attendance.service.AttendanceRecordService; import com.demo.attendance.vo.AttendanceRecordVO; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import java.time.LocalDate; import java.util.List; /** * 移动端考勤记录接口。 * 与真实项目的 /app/kqAttendanceRecord 职责一致接收现场打卡、查询日记录。 */ Slf4j RestController RequiredArgsConstructor RequestMapping(/app/attendanceRecord) public class AppAttendanceRecordController { private final AttendanceRecordService attendanceRecordService; /** * 员工打卡。 * * param dto 移动端采集的现场数据 * return 结果提示 */ PostMapping(/clock) public String clock(RequestBody Valid ClockInDTO dto) { String loginUserId demo-user; log.info(收到移动端打卡数据 userId{}, clockType{}, time{}, loginUserId, dto.getClockType(), dto.getClockTime()); return attendanceRecordService.clock(dto, loginUserId); } /** * 查询某天打卡记录。 * * param date yyyy-MM-dd * return 当天记录 */ GetMapping(/dayRecord) public ListAttendanceRecordVO dayRecord(RequestParam String date) { return attendanceRecordService.dayRecord(demo-user, LocalDate.parse(date)); } }9. PC 后台 API列表、详情和编辑import { defHttp } from //utils/http/axios; enum Api { list /biz/kqAttendanceRecord/list, detail /biz/kqAttendanceRecord/queryById, update /biz/kqAttendanceRecord/edit, exportXls /biz/kqAttendanceRecord/exportXls, } /** * PC 后台考勤记录列表。 * 用于管理员按人员、部门、打卡状态和时间范围查询。 */ export const listAttendanceRecord (params) { return defHttp.get({ url: Api.list, params }); }; /** * 查询单条考勤详情。 */ export const getAttendanceRecordDetail (params) { return defHttp.get({ url: Api.detail, params }); }; /** * 管理端修正记录。 * 实际项目里建议所有修正都走审批或留审计日志避免直接改结果。 */ export const updateAttendanceRecord (params) { return defHttp.put({ url: Api.update, params }); }; export const exportAttendanceRecordUrl Api.exportXls;10. uni-app 移动端调用真实项目的 api.js 里有const API { clock: /zhkq-api/app/kqAttendanceRecord/clock, dayRecord: /zhkq-api/app/kqAttendanceRecord/dayRecord, monthRecord: /zhkq-api/app/kqAttendanceRecord/monthRecord, abnormalList: /zhkq-api/app/kqAttendanceRecord/abnormalList }脱敏后的页面调用可以这样写function submitClock(clockInfo, location, fileList) { const formData { ruleId: clockInfo.ruleId, workId: clockInfo.workId, clockType: clockInfo.clockType, upDownWorkClock: clockInfo.upDownWorkClock, clockTime: new Date().toISOString().slice(0, 19).replace(T, ), longitude: location.longitude, latitude: location.latitude, clockAddress: location.address, clockImg: fileList.map(file file.url).join(,), attendanceContent: clockInfo.remark }; return http.post(clock, formData).then(() { uni.showToast({ title: 打卡成功 }); }); }三、这套 Demo 对应真实项目里的技术特色第一移动端只负责采集不负责最终裁判。uni-app 页面会采集时间、经纬度、图片、外勤说明、规则 ID 和班次 ID但最终是否迟到、早退、旷工、缺卡应该由后端统一判断。真实项目中还会校验手机系统时间避免员工把时间调到未来。第二记录必须保留规则快照。attendanceRule 和 attendanceWork 看起来只是两个 ID但非常关键。员工 7 月 1 日打卡时命中的规则和 7 月 10 日管理员修改后的规则可能不是一回事。历史记录必须能解释当时为什么判定正常或异常。第三异常不是后台直接改字段。真实项目里 afStatus 用来表示申诉状态。一个异常考勤记录如果直接被管理员改成正常短期省事长期会失去可信度。更好的方式是员工发起申诉主管审批审批通过后再回写记录和日统计。第四记录和统计要分层。AttendanceRecord 是事件AttendanceStats 是结果。一个员工一天可能有上班卡、下班卡、外勤卡、补卡、请假记录。如果列表页每次都实时计算查询会越来越重。更稳的方式是记录变动后重算当天统计。第五后台任务负责兜底。智慧考勤项目里有 XXL-Job 生成缺卡、旷工、历史回算等任务。移动端没有提交不代表当天没有考勤结果。系统必须在关键时间点自动补偿否则月底就会变成 HR 手工补账。四、迁移到其他企业系统时怎么复用这条链路可以抽象成一句话现场采集事件后端验证规则流程处理异常统计沉淀结果后台任务补偿缺口。巡检系统可以这样迁移AttendanceRecord 换成 InspectionRecordclockType 换成 inspectionTypeattendanceRule 换成 inspectionPlanclockImg 保留为现场照片AttendanceStats 换成巡检日报缺卡任务换成漏检任务设备保养系统也可以这样迁移规则表定义保养周期移动端提交保养位置、照片和说明后端判断是否超期异常进入审批定时任务生成逾期保养记录PC 后台按设备、人员、区域做统计五、落地时最容易踩的坑1. 不要让移动端直接传“正常/迟到/旷工”结果。移动端可以给建议状态最终状态必须由后端判断。2. 不要只存当前组织名称。人员调岗后历史统计还要能按当时部门解释。3. 不要把异常处理做成管理员后台手工改字段。必须留申请、审批、回写和审计。4. 不要只做打卡流水不做日统计。数据量一大报表会越来越慢。5. 不要忽略定时任务。缺卡、旷工、超时、漏检这类结果很多时候不是用户主动提交出来的而是系统补偿生成的。六、结论智慧考勤的可复用价值不在“打卡”两个字而在它把企业系统里最难处理的几件事串起来了规则配置现场采集证据留存后端裁判异常流程统计回算定时补偿如果你在做巡检、外勤、工单、门店、设备保养或现场服务系统可以直接复用这套工程骨架。页面可以变业务名可以变但“事件、规则、流程、统计、补偿”这五层不要省。省掉哪一层后面都会在对账、争议和报表里补回来。

相关新闻