封面

外勤打卡看起来只是“员工不在公司,也能打一次卡”,真正做系统时会发现它比普通坐班打卡复杂得多。普通打卡的核心判断是:人在不在考勤区域、是不是在允许时间内。外勤打卡的核心判断却变成了:这个人为什么不在考勤区域、他去哪里办事、现场有没有证据、记录是否需要主管确认、最终如何进入月度考勤统计。

如果只给移动端加一个“外勤打卡”按钮,短期能上线,长期一定会出问题。员工会说“我明明去客户现场了”,主管会说“我看不到证据”,HR 会说“月底统计不知道算正常还是异常”,老板会说“系统买了还是要人工对账”。所以外勤考勤不是一个按钮,而是一条证据链。

这篇结合智慧考勤项目里的真实代码,拆一下外勤打卡从移动端到后端再到审批统计的设计方式。项目涉及 `uni-app + Vue3` 移动端、`Vue3 + Ant Design Vue` PC 后台、`Spring Boot / JeecgBoot / MyBatis Plus` 后端。重点不是炫技术,而是看一个企业级考勤系统如何把外勤这种容易扯皮的场景做成可追溯流程。

核心流程

一、外勤打卡首先要和规则模型绑定

外勤不是所有人都可以随便打,也不是固定岗位员工永远不能打。系统要先有规则模型,区分“固定岗位”和“常规外勤”。智慧考勤 PC 后台规则弹窗里有几个关键字段:


{

  label: '规则类型',

  field: 'ruleType',

  component: 'JDictSelectTag',

  componentProps: {

    dictCode: 'attendanceRule_type',

    disabled: true,

    placeholder: '请选择规则类型'

  }

},

{

  label: '打卡区域',

  field: 'areaName',

  component: 'Input'

},

{

  label: '打卡时间',

  field: 'timeName',

  component: 'Input'

},

{

  label: '轨迹上报频率(秒)',

  field: 'trajectoryFrequency',

  component: 'InputNumber'

}

这几个字段背后其实是三个判断维度:

1. 人属于什么规则,是固定岗位还是常规外勤。

2. 这个规则是否绑定打卡区域和打卡时间。

3. 是否需要轨迹上报,多久上报一次。

很多考勤系统做不好外勤,是因为把“外勤打卡”当成普通打卡的例外情况。更合理的做法是把外勤做成规则的一种类型。规则决定员工在哪些时间、哪些位置、以什么方式提交考勤数据。这样后续统计和审核才有依据。

后台保存规则时也会写入规则类型名称:


values.attendanceTypeName = type.value;

values.attendanceType = type.value == '常规考勤' ? 1 : type.value == '排班考勤' ? 2 : 3;

values.ruleTypeName = values.ruleType == 1 ? '固定岗位' : '常规外勤';

这段代码看起来很简单,但对业务很关键。`ruleTypeName` 后续会影响移动端怎么提示、后端怎么判断、固定岗人员打外勤卡时是否触发审批。外勤不是“系统放松了”,而是“系统换了一套证据规则”。

二、移动端表单不能只收位置,还要收原因和图片

智慧考勤移动端外勤页面在 `zhkq-uniapp/src/pages/wqdk/clockIn.vue`。页面上除了打卡时间和定位地址,还强制要求填写外勤事由,并支持最多 4 张图片。


<textarea

  class="textarea-value"

  placeholder-class="textarea-placeholders"

  v-model="form.attendanceContent"

  placeholder="描述外勤工作内容"

  maxlength="200" />



<u-upload

  width="210"

  height="210"

  :fileList="fileList"

  @afterRead="afterRead"

  @delete="deletePic"

  multiple

  :maxCount="4"

  :maxSize="5242880"

  :previewFullImage="true"

  :capture="['camera']">

</u-upload>

为什么一定要有 `attendanceContent`?因为定位只能证明“人在某个地方”,不能证明“为什么在这个地方”。外勤打卡真正需要解释的是业务背景:拜访客户、现场维修、项目巡检、临时支援、外出送资料,每一种场景对主管审核的含义都不一样。

图片字段 `clockImg` 也不是装饰。外勤现场经常发生两个问题:一是定位漂移,二是事后说不清。图片能补足位置数据的不足。比如客户门头、现场设备、施工进度、会议签到台,这些材料能让主管不用再追问员工,也能让 HR 月底复核时有依据。

移动端初始化表单时,直接把外勤打卡的关键字段结构化:


const form = ref({

  attendanceContent: "",

  clockAddress: province ? [province, city, district, street, streetNum, poiName].join('') : "",

  clockImg: "",

  clockTime: clockDate.Y_M_D_H_I_S,

  clockType: "2",

  clockTypeName: "外勤打卡",

  latitude,

  longitude,

  ruleId: clockInfo.ruleId || "",

  upDownWorkClock: clockInfo.type,

  upDownWorkClockName: clockInfo.typeText,

});

这里建议关注 `clockType: "2"`、`clockTypeName: "外勤打卡"`、`ruleId`、`upDownWorkClock` 这几个字段。它们决定了这条记录不是普通位置记录,而是一条可以进入考勤规则、上下班类型、统计结果的业务记录。

三、图片上传要带上时间、地址和经纬度

外勤图片如果只是普通附件,价值会下降。智慧考勤在上传图片时,把地址、经纬度、时间一起带到上传接口。


const afterRead = async (event) => {

  let params = {

    address: form.value.clockAddress,

    latitude: form.value.latitude,

    longitude: form.value.longitude,

    date: timestampToDate(Date.now()).Y_M_D_H_I_S,

  }

  fileList.value.push(...event.file.map(file => {

    file.params = { ...params };

    return file;

  }));

  uploadFileList(fileList)

}

上传接口参数里拼接这些信息:


uni.uploadFile({

  url: BASE_URL + $API.uploadFile

    + `?address=${address}&latitude=${latitude}&longitude=${longitude}&date=${date}`,

  filePath: file.url,

  name: 'file',

  header: {

    ['X-Access-Token']: loginStore.accessToken,

  },

  success(e) {

    let result = JSON.parse(e.data).result

    file.url = result;

    resolve(result);

  }

})

这样做的好处是图片不再是“孤立文件”,而是能和外勤打卡的地点、时间形成关联。实际系统里,如果后续要做水印、审计、图片溯源、审核留痕,这些字段都能派上用场。

这也是很多企业系统的通用经验:附件不要只保存 URL。只保存 URL,后续只能证明“上传过一张图”;关联业务上下文后,才能证明“某人在某时某地为某个业务动作上传了这张图”。

四、提交前必须校验事由和位置

移动端提交时,智慧考勤做了两个基础校验:外勤事由不能为空,定位信息不能为空。


function submit(){

  let { attendanceContent } = form.value;

  if(attendanceContent.trim().length == 0){

    $msg.warring("外勤事由不能为空")

    return;

  }else if(!latitude || !latitude){

    $msg.warring("未获取到当前位置信息")

    return;

  }

  if(submitStatus != 0) return;

  submitStatus = 1;

  uploadFileList(fileList).then(() => {

    return $http.post('clock', {

      ...form.value,

      clockImg: fileList.value.map(file => file.url).join(','),

    }, {

      isError: false,

    });

  })

}

这里还有一个小细节:`submitStatus` 用来防止重复提交。打卡类系统最怕用户连续点按钮,尤其是在移动网络不稳定时,重复请求会导致多条记录、审批重复、统计异常。虽然这不是最终的幂等方案,但前端先挡一层是必要的。

更完整的工程设计,还应该在后端对同一人员、同一规则、同一上下班类型、短时间窗口做幂等控制。前端负责用户体验,后端负责最终一致性。

五、网络异常时要缓存,不能让员工丢记录

外勤场景经常发生网络不稳定。客户现场地下室、工地、电梯、偏远园区都可能没网。如果系统要求必须实时上传,员工就会陷入尴尬:人已经到了,业务也做了,但系统不让打。

智慧考勤移动端有缓存数据页面,展示“考勤打卡缓存数据”和“轨迹定位缓存数据”:


<view class="tips">

  您当前有

  <text class="num">{{locationStore.clockingInPoop.length}}</text>

  条考勤打卡数据待上传

</view>



<view class="tips">

  您当前有

  <text class="num">{{locationStore.officeTrackPool.length + (locationStore.currentTrack ? 1 : 0)}}</text>

  条办公轨迹记录,以及

  <text class="num">{{locationStore.coordinatePool.length}}</text>

  个轨迹点位数据待上传

</view>

外勤提交失败时,移动端还会把待上传的图片和表单一起缓存:


locationStore.addClockingIn({

  ...form.value,

  clockImg: fileList.value.map(file => {

    return {

      url: file.url,

      params: file.params,

    }

  }),

})

这类设计对外勤特别重要。考勤系统不能只站在服务器视角,也要站在员工现场视角。没有网络时,系统应该告诉员工“当前为离线模式,数据会缓存,联网后同步”,而不是让员工自己截图、打电话、找 HR 说明情况。

六、后端入参要保留证据字段

后端入参在 `AppKqAttendanceRecordIn` 里,外勤相关字段比较清晰:


@ApiModelProperty(value = "打卡类型1、单位打卡 2、外勤打卡 3、紧急外勤打卡 4、外出打卡 5.固定岗外勤")

@NotBlank(message = "打卡类型不能为空")

private String clockType;



@ApiModelProperty(value = "打卡类型名称")

@NotBlank(message = "打卡类型不能为空")

private String clockTypeName;



@ApiModelProperty(value = "打卡时间")

@NotNull(message = "打卡时间不能为空")

private Date clockTime;



@ApiModelProperty(value = "打卡地理信息")

private String clockAddress;



@ApiModelProperty(value = "经度")

private String longitude;



@ApiModelProperty(value = "纬度")

private String latitude;



@ApiModelProperty(value = "打卡照片")

private String clockImg;



@ApiModelProperty(value = "考勤内容")

private String attendanceContent;

这组字段基本覆盖了外勤证据链:类型、时间、地址、经纬度、照片、事由。很多低质量系统只保存时间和定位,导致后续申诉时无法解释。外勤打卡必须把“人、时间、地点、原因、材料、规则”同时保存下来。

七、`/clock` 接口统一处理单位打卡和外勤打卡

后端接口在 `AppKqAttendanceRecordController`:


@PostMapping(value = "/clock")

public Result<String> clock(@RequestBody @Valid AppKqAttendanceRecordIn in) {

    LoginUser user = DataAuthUtil.getCurrentUser();

    if(null == in.getClockTime()){

        in.setClockTime(new Date());

    }



    KqAttendanceRecord kqAttendanceRecord = BeanUtil.toBean(in, KqAttendanceRecord.class);

    kqAttendanceRecord.setAttendanceRule(in.getRuleId());



    if (StrUtil.isBlank(kqAttendanceRecord.getClockAddress())

            && StrUtil.isNotBlank(kqAttendanceRecord.getLongitude())

            && StrUtil.isNotBlank(kqAttendanceRecord.getLatitude())) {

        Map<String, String> map = geoUtils.getCityByLonLat(

                kqAttendanceRecord.getLongitude(),

                kqAttendanceRecord.getLatitude());

        if (null != map) {

            kqAttendanceRecord.setClockAddress(map.get("address"));

        }

    }



    if (kqAttendanceRecord.getClockTime().getTime() > System.currentTimeMillis() + 600000L) {

        throw new RuntimeException("打卡失败!请校准手机系统时间");

    }



    if (EClockType.NORMAL.getValue().equals(in.getClockType())

            || EClockType.LEGWORK.getValue().equals(in.getClockType())) {

        return kqAttendanceRecordService.clock(kqAttendanceRecord);

    }

}

这段代码有几个值得借鉴的点:

1. 前端没传地址时,后端根据经纬度反查地址,提升数据完整性。

2. 打卡时间不能超过当前时间 10 分钟,防止用户篡改手机时间。

3. 单位打卡和外勤打卡走统一入口,但通过 `clockType` 区分业务。

4. `ruleId` 被写入 `attendanceRule`,后续统计能知道记录属于哪条规则。

统一入口的好处是移动端调用简单,后端也能集中做日志、校验、补充地址、时间防篡改等通用逻辑。差异化逻辑放到 Service 层和枚举里处理。

八、固定岗人员打外勤卡要触发审批

外勤最容易被滥用的场景是固定岗位员工点击外勤打卡。固定岗位不是不能外出,但应该有审核。智慧考勤在 Service 里判断:如果当前规则是固定岗,并且打的是外勤卡,就生成固定岗外勤审核记录。


if ("1".equals(kqRule.getRuleType()) && "2".equals(kqAttendanceRecord.getClockType())) {

    Thread t = new Thread(new Runnable() {

        @Override

        public void run() {

            kqAfOutRecordService.saveOutRecord(kqAttendanceRecord);

            log.info("新增了一条固定岗外勤打卡审核记录:{}", kqAttendanceRecord);

        }

    });

    t.start();

}

审批记录的保存逻辑在 `KqAfOutRecordServiceImpl`:


public void saveOutRecord(KqAttendanceRecord record) {

    KqAfOutRecord outRecord = new KqAfOutRecord();

    BeanUtils.copyProperties(record, outRecord);

    outRecord.setClockType(EClockType.GUDING_LEGWORK.getValue());

    outRecord.setClockTypeName(EClockType.GUDING_LEGWORK.getName());

    outRecord.setAttendanceRecordId(record.getId());



    BizFlowContext flowContext = new BizFlowContext();

    flowContext.setActDefKey(FlowKey.AF_KQ_OUT.getValue());

    flowContext.setBizData(JSONUtil.toJsonStr(outRecord));

    flowContext.setActDefName("固定岗外勤打卡");



    LoginUser user = DataAuthUtil.getCurrentUser();

    PlatformResult result = bpmFlowOptService.startFlow(flowContext, user.getUsername());

    if(!result.isSuccess()){

        throw new ExpressionException(result.getMsg());

    }

}

这就是外勤打卡从“记录”走向“流程”的关键。对于常规外勤人员,外勤记录可以直接进入统计;对于固定岗位人员,外勤记录需要进入审批。系统不是一刀切,而是根据岗位规则决定后续处理方式。

九、PC 后台要能查规则、查打卡、查轨迹

外勤不是移动端一个人的事。后台管理端需要能看到规则、打卡记录和定位轨迹。智慧考勤 PC 维护首页接口里有这些查询:


enum Api {

  rulelist = '/biz/kqAfRule/getRuleByUserId',

  clockList = '/biz/kqAttendanceRecord/list1?column=createTime&order=desc',

  locationList = '/biz/kqLocationInfoReocord/list1?column=createTime&order=desc',

}



export const rulelistApi = (params) => defHttp.get({ url: Api.rulelist, params });

export const clockListApi = (params) => defHttp.get({ url: Api.clockList, params });

export const locationListApi = (params) => defHttp.get({ url: Api.locationList, params });

这说明外勤证据链至少有三类后台视角:

1. 规则视角:这个人应该按什么规则考勤。

2. 记录视角:他什么时候打了什么类型的卡。

3. 轨迹视角:他的定位过程是否能还原现场。

如果后台只看最终结果,主管和 HR 都没法判断争议。后台必须能向上追溯规则,向下查看记录和轨迹。

十、外勤打卡真正要解决的是“可解释”

做外勤考勤时,不要把目标写成“定位更准”。定位精度当然重要,但真实企业管理里,争议往往不是一个坐标点能解决的。员工需要解释现场情况,主管需要判断是否认可,HR 需要月度统计,老板需要降低管理成本。

所以外勤打卡的完整设计应该包括:

1. 规则层:区分固定岗位、常规外勤、排班和值班。

2. 移动端:收集时间、位置、事由、图片、上下班类型。

3. 上传层:图片和地址、经纬度、时间绑定。

4. 后端层:统一 `/clock` 接口,做时间防篡改、地址补全、类型分发。

5. 审批层:固定岗外勤自动进入审批流。

6. 后台层:支持规则、打卡记录、轨迹查询。

7. 统计层:最终进入日统计、月统计和异常处理。

如果只做第 2 步,系统很快会变成“员工提交,主管人工判断”。如果把 1 到 7 步串起来,外勤考勤才会从“打一次卡”变成“可追溯、可审核、可统计”的管理闭环。

十一、可复用到其他企业系统的经验

外勤打卡这套设计不只适用于考勤。很多企业系统都有类似场景:销售拜访、巡检任务、物业维修、养老院护理外出、项目现场验收、门店督导、司机配送。只要业务发生在现场,就不能只保存一条状态。

可复用的设计原则是:

1. 现场业务必须保存位置,但不能只保存位置。

2. 图片要和业务上下文绑定,不要只保存附件 URL。

3. 规则要决定流程,不能所有人走同一套审核。

4. 移动端要处理离线和重复提交。

5. 后台要能追溯规则、记录、轨迹和审批。

6. 统计结果要能回看原始证据。

这也是智慧考勤系统和普通打卡工具的差别。普通工具解决“有没有打卡”,企业级系统解决“这条记录为什么可信、出了争议怎么复核、月底怎么自动统计”。

外勤考勤的难点不在一个按钮,而在证据链。把证据链设计清楚,代码才有结构,系统才有生命力。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐