
很多考勤系统做不好,不是因为少了一个打卡按钮,而是因为 PC 后台、移动端、后端接口和统计报表各自为政。员工在手机上打卡,主管在流程里审批,人事在 PC 后台查记录,月底还要导出统计表。如果这些端对"迟到、缺卡、请假、补卡、外勤、申诉"的理解不一致,系统越上线越像一堆孤立页面。
我最近梳理智慧考勤项目时,第 9 篇最值得写的不是"PC 和移动端都有页面",而是它把多端职责拆得比较清楚:`zhkq-web` 负责规则和管理,`zhkq-uniapp` 负责现场采集和员工操作,`zhkq-api` 负责可信判断、流程回写和统计输出。这个分层对很多企业管理系统都能复用。

一、为什么考勤一定会变成多端协同问题?
考勤的输入来自现场,但规则不在现场。
员工打卡发生在手机上:定位、WiFi、蓝牙、拍照、外勤事由、请假申请、异常申诉,都更适合放在移动端。员工要的是"现在能不能打卡""为什么不能打卡""异常怎么提交"。
考勤规则维护发生在 PC 后台:考勤区域、考勤时间、WiFi、蓝牙、考勤规则、排班、组织、岗位、人员变动、日统计、轨迹查看,都更适合用表格、筛选、地图和批量操作来处理。
最终判断必须回到后端:移动端可以做提示,但不能成为最终裁判。用户手机时间可能不准,定位可能漂移,离线缓存可能延迟上传,人员规则可能在当天发生变更。后端必须重新基于规则、人员、时间和状态计算一次。
这个项目的目录正好对应三层职责:
zhkq-web
src/views/attendance/
src/views/attendanceRecord/
src/views/task/
zhkq-uniapp
src/pages/dwdk/
src/pages/wqdk/
src/pages/qjgl/
src/pages/abnormalAppealList/
src/pages/cache-data/
zhkq-api
jeecg-module-zhkq/src/main/java/org/jeecg/modules/biz/
app/controller/
manage/
entity/
enums/
xxljob/
这不是简单的前后端分离,而是按业务角色拆端。
二、PC 后台:负责规则、记录、统计和权限边界
PC 后台不是给员工日常打卡用的,它更像考勤系统的"控制台"。在智慧考勤里,PC 端主要集中在这些目录:
zhkq-web/src/views/attendance/area/
zhkq-web/src/views/attendance/wifi/
zhkq-web/src/views/attendance/bluetooth/
zhkq-web/src/views/attendance/time/
zhkq-web/src/views/attendance/rule/
zhkq-web/src/views/attendanceRecord/clockIn/
zhkq-web/src/views/attendanceRecord/daily/
zhkq-web/src/views/attendanceRecord/trackRecord/
zhkq-web/src/views/attendanceRecord/trackView/
这些页面背后对应的是"规则建模"和"结果查询"两类工作。
比如考勤记录列表接口在 PC 端是这样封装的:
// zhkq-web/src/views/attendanceRecord/clockIn/clock.in.api.ts
enum Api {
list = '/biz/kqAttendanceRecord/list',
updateKqAttendanceRecord = '/biz/kqAttendanceRecord/edit',
exportXls = '/biz/kqAttendanceRecord/exportXls',
queryDepartTreeSync = '/sys/sysDepart/queryDepartTreeSync',
handleDetail = '/biz/kqAttendanceRecord/queryById'
}
export const list = (params) => defHttp.get({ url: Api.list, params });
export const updateKqAttendanceRecord = (params) =>
defHttp.put({ url: Api.updateKqAttendanceRecord, params });
这段代码不复杂,但它说明了 PC 后台的职责:按组织、人员、时间、状态筛选记录;必要时进行人工修正;最后支持导出。它不应该负责判断员工当前能不能打卡,也不应该负责采集定位。
每日统计也是同样思路:
// zhkq-web/src/views/attendanceRecord/daily/daily.api.ts
enum Api {
list = '/biz/kqAttendanceDayStats/list',
exportXls = '/biz/kqAttendanceDayStats/exportXls',
queryDepartTreeSync = '/sys/sysDepart/queryDepartTreeSync'
}
export const list = (params) => defHttp.get({ url: Api.list, params });
export const getExportUrl = Api.exportXls;
后台关注的是"结果能不能解释、能不能筛选、能不能导出"。如果把这些功能塞进移动端,员工端会变重;如果 PC 后台只做展示,不处理权限和统计,又会让 HR 回到 Excel。
三、uni-app 移动端:负责现场动作和员工可理解的反馈
移动端的核心价值是采集现场事实。智慧考勤的 `zhkq-uniapp` 里,和考勤闭环相关的页面包括:
src/pages/dwdk/index.vue
src/pages/wqdk/clockIn.vue
src/pages/qjgl/appeal.vue
src/pages/abnormalAppealList/index.vue
src/pages/abnormalAppealList/appeal-detail.vue
src/pages/officeTrajectory/index.nvue
src/pages/cache-data/index.vue
移动端必须比 PC 端更重视提示和容错。例如 `cache-data/index.vue` 就把离线场景单独做成页面,明确告诉员工当前有多少打卡缓存、多少轨迹记录、多少轨迹点位等待同步:
<view class="title">考勤打卡缓存数据</view>
<view class="tips">
您当前有
<text class="num">{{locationStore.clockingInPoop.length}}</text>
条考勤打卡数据待上传
</view>
<view class="title">轨迹定位缓存数据</view>
<view class="tips">
您当前有
<text class="num">
{{locationStore.officeTrackPool.length + (locationStore.currentTrack ? 1 : 0)}}
</text>
条办公轨迹记录,以及
<text class="num">{{locationStore.coordinatePool.length}}</text>
个轨迹点位数据待上传
</view>
这个设计很实际。外勤场景下手机网络不稳定是常态,如果系统只提示"提交失败",员工会以为自己没打上卡;如果系统明确展示缓存数据,员工至少知道记录还在本机,后续可以同步。
请假页面也是移动端职责的典型例子。它不只是一个表单,而是要把员工能理解的字段收齐:
<!-- zhkq-uniapp/src/pages/qjgl/appeal.vue -->
<picker :range="leaveTypeList" range-key="title" @change="confirmType">
<view :class="!form.leaveType?'opcity':''">
{{!form.leaveTypeName?'请选择请假类型':form.leaveTypeName}}
</view>
</picker>
<textarea
class="textarea-value"
v-model="form.leaveArgument"
placeholder="事由说明"
maxlength="200" />
<u-upload
:fileList="fileList"
@afterRead="afterRead"
@delete="deletePic"
multiple
:maxCount="4"
:previewFullImage="true" />
这里的关键不是"有请假功能",而是移动端要让员工一次说清楚请假类型、开始结束日期、半天/全天、事由、图片材料。字段收不齐,后端流程就会变成反复退回;移动端体验不好,员工就会绕回聊天软件里说。
四、后端:统一接口语义,避免每端各算各的
多端协同最大的坑,是每端都有一套自己的状态理解。智慧考勤后端把常用状态收敛到枚举里,例如:
// org.jeecg.modules.biz.enums.EClockStatus
public enum EClockStatus {
NORMAL(0, "正常"),
LATE(1, "迟到"),
LACK(2, "缺卡"),
LEAVE(3, "早退"),
ABSENTEEISM(4, "旷工"),
ASKFORLEAVE(5, "请假"),
REISSUECARD(6, "补卡");
}
状态必须由后端统一维护。否则 PC 后台显示"补卡",移动端显示"正常",日统计又算成"缺卡",月底必然吵起来。
后端接口也体现了职责分离。移动端请假走 `/app/kqAfLeaveRecord`,后台记录查询走 `/biz/kqAttendanceRecord`,日统计查询走 `/biz/kqAttendanceDayStats`。例如 App 请假控制器:
@RestController
@RequestMapping("/app/kqAfLeaveRecord")
public class AppKqAfLeaveRecordController {
@PostMapping(value = "/add")
public Result<String> add(@RequestBody @Valid AppKqAfLeaveRecordAddIn in) {
kqAfLeaveRecordService.appAdd(BeanUtil.copyProperties(in, KqAfLeaveRecord.class));
return Result.OK("提交成功");
}
@PostMapping(value = "/afCommit")
public Result<String> afCommit(@RequestParam(name = "id", required = true) String id) {
return kqAfLeaveRecordService.afCommit(id);
}
}
PC 后台考勤记录查询则更关注权限过滤、条件筛选和分页:
@GetMapping(value = "/list")
public Result<?> queryPageList(KqAttendanceRecordIn in,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize) {
LambdaQueryWrapper<KqAttendanceRecord> queryWrapper = new LambdaQueryWrapper<>();
if (StringUtils.isNotEmpty(in.getUnitId())) {
queryWrapper.apply("(unit_id='" + in.getUnitId() + "' or department_id='" + in.getUnitId() + "')");
} else {
List<SysDepart> sysDeparts = iKqUserUnitService.queryDeparts(DataAuthUtil.getCurrentUserId());
if (CollUtil.isNotEmpty(sysDeparts)) {
List<String> collect = sysDeparts.stream().map(SysDepart::getId).collect(Collectors.toList());
queryWrapper.in(KqAttendanceRecord::getDepartmentId, collect);
}
}
queryWrapper.orderByDesc(KqAttendanceRecord::getClockTime);
queryWrapper.select(KqAttendanceRecord::getId);
IPage<KqAttendanceRecord> pageList = kqAttendanceRecordService.page(new Page<>(pageNo, pageSize), queryWrapper);
return Result.OK(pageList);
}
这里有一个值得注意的工程点:查询先只 select `id`,分页后再 `listByIds` 获取详情。这样做通常是为了减少复杂字段分页时的负担,也方便后续扩展详情装配。
五、一次异常补卡如何跨端流转?
以"员工发现自己缺卡,提交异常申诉"为例,多端链路大致是:
移动端异常列表
-> 员工查看异常详情
-> 提交异常申诉和图片
-> 后端创建申诉记录并进入流程
-> 主管或后台审批
-> 后端回写考勤记录状态
-> 日统计重新体现结果
-> PC 后台可查询、导出、追溯
PC 后台编辑考勤记录时,如果状态变化,还会触发流程成功后的同步处理:
KqAttendanceRecord db = kqAttendanceRecordService.getById(kqAttendanceRecord.getId());
kqAttendanceRecordService.updateById(kqAttendanceRecord);
if (db.getClockStatus() != kqAttendanceRecord.getClockStatus()) {
QueryWrapper<KqAttendanceRecord> wrapper = new QueryWrapper<>();
wrapper.eq("kq_attendance_record.id", kqAttendanceRecord.getId());
List<KqCountDetailsVO> kqCountDetails = kqAttendanceRecordService.getKqCountDetails(wrapper);
KqCountDetailsVO kqCountDetailsVO = kqCountDetails.get(0);
afAbnormalAppealFlowService.handleSuccessful(kqCountDetailsVO, bizData, kqAttendanceRecord);
}
这段逻辑说明一个事实:考勤记录不是静态表。它会被请假、返岗、外勤、申诉、审批、补卡、定时任务共同影响。多端系统如果没有统一后端兜底,就很容易出现"页面改了,统计没变""审批通过了,报表还是异常"的问题。
六、日统计:让 PC 后台看到可解释结果
考勤闭环最终要落到统计。智慧考勤的日统计后台接口同样做了组织权限过滤、人员姓名过滤、日期过滤和状态过滤:
if (StringUtils.isNotEmpty(in.getClockTime())) {
queryWrapper.apply(
"(date_format(clock_in_time1,'%Y-%m-%d')='" + in.getClockTime() + "' " +
"or date_format(clock_in_time2,'%Y-%m-%d')='" + in.getClockTime() + "' " +
"or date_format(clock_out_time1,'%Y-%m-%d')='" + in.getClockTime() + "' " +
"or date_format(clock_out_time2,'%Y-%m-%d')='" + in.getClockTime() + "')");
}
if (StringUtils.isNotEmpty(in.getClockStatus())) {
queryWrapper.apply(
"(clock_in_type1='" + in.getClockStatus() + "' " +
"or clock_in_type2='" + in.getClockStatus() + "' " +
"or clock_out_type1='" + in.getClockStatus() + "' " +
"or clock_out_type2='" + in.getClockStatus() + "')");
}
从技术角度看,这类查询后续还有优化空间,例如避免字符串拼接式 `apply`,改成参数化条件,减少 SQL 注入和索引失效风险。但从业务角度看,它表达了一个正确方向:日统计不只是汇总数字,还要能按具体日期、人员、状态和组织追溯回去。
七、多端协同的 6 个设计原则
我把这个项目里的经验总结成 6 条。
第一,移动端只做现场采集和用户反馈,不做最终裁判。它可以提示定位不在范围、当前不在考勤时间,但最终是否有效必须由后端确认。
第二,PC 后台负责规则和结果,不要让员工端承担复杂配置。考勤区域、时间、规则、排班、组织、统计、轨迹回放,都适合 PC 端处理。
第三,状态枚举必须统一。迟到、缺卡、早退、旷工、请假、补卡这类状态不能散落在多个前端常量里,后端应成为最终语义来源。
第四,流程要能回写记录。请假、返岗、异常申诉如果只保存在流程表里,不回写考勤记录和日统计,HR 月底还是要手工合并。
第五,离线缓存要被用户看见。移动端离线时,不只要缓存数据,还要让员工知道缓存了多少条、何时需要同步。
第六,后台查询必须带权限边界。考勤数据涉及人员位置、出勤状态和异常原因,不能让所有后台账号看全量数据。
八、可复用到其他企业系统的经验
考勤只是一个入口。类似的多端协同问题,在巡检、设备维保、门店拜访、项目现场、护理记录、车辆管理里都会出现。
如果你在设计企业系统,可以照这个模型拆:
移动端:采集事实、提示异常、提交申请、查看进度
PC 后台:配置规则、审核流程、查询记录、导出报表
后端服务:统一状态、执行规则、写入记录、回写统计
定时任务:补偿缺失、重算历史、生成汇总
权限体系:按组织、部门、岗位过滤数据
好的多端系统不是把同一个页面复制到手机和 PC,而是让不同角色各做自己最适合做的事。员工端要轻,后台端要稳,后端要统一裁判,统计要可追溯。智慧考勤系统的价值也正在这里:它不是只记录谁点了打卡,而是把规则、现场、流程、审批和报表接成了一条能解释的业务链路。