☰
人到了没打卡?xllg 打卡签到防作弊:时间窗口 + 定位围栏双校验
2026/10/14 9:27:35 网站建设 项目流程

人到了没打卡?xllg 打卡签到防作弊:时间窗口 + 定位围栏双校验

导读:零工场景最怕两件事:人没到岗却打了卡,或者到了岗忘了打卡。xllg 的打卡设计是"时间窗口 + 定位围栏"双条件:必须在岗位附近、必须在规定时段内,两个都满足才记录成功。这篇把校验规则、表设计和一个真实事故讲清楚。

零工打卡和办公室打卡不一样。工厂流水线的考勤机在固定点位,零工岗位可能是一个工地、一个仓库、一个门店,位置是散的。所以 xllg 的打卡记录必须带坐标,校验逻辑也围绕坐标展开——位置不对,时间再准也不算数。

表结构:打卡记录带坐标和时间

打卡不只是存一个时间戳。谁打的、打哪个岗、在哪打的、什么时间,这些字段缺一个,事后对账就说不清:

CREATETABLEjx_checkin(idBIGINTUNSIGNEDAUTO_INCREMENTPRIMARYKEY,order_idBIGINTUNSIGNEDNOTNULLCOMMENT'用工单id',worker_idBIGINTUNSIGNEDNOTNULLCOMMENT'工人id',check_typeTINYINTNOTNULLCOMMENT'1上班打卡 2下班打卡',latDECIMAL(10,6)NOTNULLCOMMENT'打卡纬度',lngDECIMAL(10,6)NOTNULLCOMMENT'打卡经度',distance_mINTNOTNULLDEFAULT0COMMENT'距岗位中心距离(米)',check_timeDATETIMENOTNULLCOMMENT'打卡时间(服务器时间)',resultTINYINTNOTNULLDEFAULT0COMMENT'0校验失败 1校验通过',fail_reasonVARCHAR(100)DEFAULTNULLCOMMENT'失败原因(越界/超时/重复)',create_timeDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMP,KEYidx_order_worker(order_id,worker_id),KEYidx_worker_time(worker_id,check_time))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COMMENT='零工打卡记录';

打卡时间用服务器时间,不用客户端时间——客户端时间用户能改,改到上班时间点前就能"按时打卡"。这是第一道防作弊的底线。

校验逻辑:两个条件,缺一不可

上班打卡校验:时间在允许窗口内(比如开工前 30 分钟到开工后 15 分钟),且距离在岗位围栏内(比如 500 米)。下班打卡同理:

@ServicepublicclassCheckinService{@Value("${xllg.checkin.radius:500}")privateintradiusMeter;// 围栏半径:默认 500 米@Value("${xllg.checkin.early-minutes:30}")privateintearlyMinutes;// 允许提前打卡:30 分钟@Value("${xllg.checkin.late-minutes:15}")privateintlateMinutes;// 允许迟到补卡:15 分钟publicCheckinResultcheckin(CheckinCommandcmd){// 1. 取用工单,拿到岗位坐标和工作时段WorkOrderorder=orderMapper.selectById(cmd.getOrderId());if(order==null||order.getStatus()!=2/*已录取*/){returnCheckinResult.fail("用工单不存在或未录取");}// 2. 时间窗口校验(服务器时间)LocalDateTimenow=LocalDateTime.now();LocalDateTimeworkStart=order.getWorkStartTime();LocalDateTimeworkEnd=order.getWorkEndTime();booleantimeOk;if(cmd.getCheckType()==1){timeOk=now.isAfter(workStart.minusMinutes(earlyMinutes))&&now.isBefore(workStart.plusMinutes(lateMinutes));}else{timeOk=now.isAfter(workEnd.minusMinutes(lateMinutes))&&now.isBefore(workEnd.plusMinutes(30));}if(!timeOk){returnCheckinResult.fail("不在打卡时间段内");}// 3. 定位围栏校验:Haversine 算球面距离doubledistance=haversine(order.getLat(),order.getLng(),cmd.getLat(),cmd.getLng());if(distance>radiusMeter){returnCheckinResult.fail("距岗位中心 "+(int)distance+" 米,超出 "+radiusMeter+" 米围栏");}// 4. 防重复打卡:同一单同一类型只能一次intdup=checkinMapper.countByOrderAndType(cmd.getOrderId(),cmd.getWorkerId(),cmd.getCheckType());if(dup>0){returnCheckinResult.fail("该时段已打过卡");}// 5. 落库checkinMapper.insert(CheckinRecord.builder().orderId(cmd.getOrderId()).workerId(cmd.getWorkerId()).checkType(cmd.getCheckType()).lat(cmd.getLat()).lng(cmd.getLng()).distanceMeter((int)distance).checkTime(LocalDateTime.now()).result(1).build());returnCheckinResult.ok();}/** Haversine 公式:返回两点球面距离,单位米 */privatedoublehaversine(doublelat1,doublelng1,doublelat2,doublelng2){doubleR=6371000;doubledLat=Math.toRadians(lat2-lat1);doubledLng=Math.toRadians(lng2-lng1);doublea=Math.sin(dLat/2)*Math.sin(dLat/2)+Math.cos(Math.toRadians(lat1))*Math.cos(Math.toRadians(lat2))*Math.sin(dLng/2)*Math.sin(dLng/2);returnR*2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a));}}

注意失败也要落库(result=0+fail_reason)。这很重要:一是给用户看"为什么打卡失败"的证据,二是运营侧可以统计越界/超时比例,反推围栏参数要不要调。打卡失败不留痕,等于没有校验。

围栏半径:一个参数,两种业态

零工岗位的"围栏"不能一刀切。建筑工地一圈 500 米,工人进出频繁,半径小了全误杀;门店里一个服务员,100 米都嫌大——隔壁奶茶店的员工都能"顺路打卡"。xllg 的做法是把半径做成用工单级配置,发岗时按业态选:

// 发岗表单:围栏半径按业态预设,商家可微调switch(order.getIndustryType()){case1:// 建筑工地:场地大,允许 800 米order.setCheckinRadius(800);break;case2:// 门店/零售:室内为主,300 米order.setCheckinRadius(300);break;case3:// 仓储/物流:库区分区明确,500 米order.setCheckinRadius(500);break;default:order.setCheckinRadius(500);}

配置比写死的优势在于可调:上线后看失败记录里fail_reason='越界'的占比,占比过高就调大对应业态的半径,过低(说明有人钻空子)就调小。数据驱动调参,而不是拍脑袋定一个值。

踩坑实录:围栏 500 米,市中心全是"越界"

现象:上线第一周,运营反馈市中心某门店的工人打卡大量失败,报"距岗位中心 xxx 米,超出 500 米围栏",但工人明明就在门店门口。

排查过程:先看失败记录里的距离——报了 600~900 米不等。再对比工人客户端上报的坐标和岗位坐标,差值稳定在几百米。用地图工具把两个坐标标出来,发现岗位坐标存的是门店收货地址(仓库后方),实际打卡点位在门店正门,直线距离 400 多米,加上 GPS 漂移(市区高楼反射,误差 50~100 米很正常),轻松突破 500 米围栏。

定位思路:不是校验逻辑错,是坐标来源错了。发岗时商家填的地址是"收货/办公地址",地理编码(转经纬度)用的是高德逆地址解析,解析结果指向的是地址文本对应的中心点,和实际营业门面差一条街。校验规则再对,坐标本身偏了,结果必然错。

最终解决:三步修正,围栏误杀率从 23% 降到 1% 以下:

  1. 坐标校准:发岗时允许商家在地图上手动拖拽钉选实际岗位坐标,不用地址解析结果兜底:
// 发岗表单里增加:岗位坐标以地图钉选为准order.setLat(markerLat);order.setLng(markerLng);
  1. 围栏参数化:不同商圈、不同业态(工地/门店/仓库)围栏半径不一样,改成按用工单配置:
ALTERTABLEjx_orderADDCOLUMNcheckin_radiusINTNOTNULLDEFAULT500COMMENT'打卡围栏半径(米)';
  1. GPS 漂移容忍:校验前先做简单平滑——取最近 30 秒内连续 3 次定位的中位数,过滤单点漂移:
// 前端:3 次定位取中位数后再提交constpts=awaitPromise.all([getLoc(),getLoc(),getLoc()]);constlat=median(pts.map(p=>p.latitude));constlng=median(pts.map(p=>p.longitude));

可以直接抄的结论

  1. 打卡时间用服务器时间,客户端时间不可信;
  2. 坐标字段DECIMAL(10,6),距离用 Haversine 球面公式算;
  3. 校验 = 时间窗口(提前/迟到分钟数)+ 定位围栏(半径参数化)+ 防重复(同单同类型唯一);
  4. 失败也落库:result+fail_reason,是调参和运营对账的依据;
  5. 岗位坐标让商家地图钉选,别依赖地址文本解析;围栏半径按用工单配置,别写死;
  6. 前端定位做 3 次取中位数,抗 GPS 漂移。

打卡防作弊不是越严越好——太严误杀正常工人,太松形同虚设。时间窗口、围栏半径、漂移容忍三者的平衡,要靠失败记录的数据持续调。

项目源码:https://gitee.com/gzqkl/xllg

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询