混合框架一码多端电子围栏打卡实战:多边形围栏、坐标校验和后端规则绑定

员工站在公司楼下,手机却提示"不在考勤范围";主管把定位半径从 100 米改到 500 米,投诉少了,但隔壁园区也能打卡;HR 到月底看异常记录,只能在员工、主管和系统之间来回确认。定位打卡最容易翻车的地方,不是"有没有调用定位接口",而是考勤范围到底怎么画、怎么保存、怎么绑定规则、怎么在移动端解释失败原因。

很多系统第一版会用"中心点 + 半径"做电子围栏。这种方案适合门店很小、边界近似圆形的场景,但企业园区、医院、养老院、仓库、工地、写字楼往往不是圆形。一栋楼可能贴着马路,后门可能紧邻另一家公司,厂区边界可能是 L 形或多栋楼组成的区域。半径一放大,误判会变少,但考勤可信度会下降;半径一缩小,误判会变多,员工体验会变差。

智慧考勤项目里没有只把围栏做成一个经纬度半径,而是把考勤区域抽成 `kq_af_area`,用地图绘制得到的多边形点位保存到 `areaJson`,再通过 `kq_rule_area` 把区域和考勤规则绑定。移动端拿到规则后,把 `areaJson` 解析成 `areaCoordinates`,用 `checkPointInPolygon(longitude, latitude, coordinates)` 判断当前位置是否落在多边形内。这个设计的重点不是算法多复杂,而是让"围栏配置、审批生效、移动端判断、异常提示、后续统计"能连成闭环。

本文基于智慧考勤项目真实代码做脱敏整理,主要参考这些文件:

  • 后端区域实体:`zhkq-api/jeecg-module-zhkq/src/main/java/org/jeecg/modules/biz/entity/KqAfArea.java`
  • 后端规则区域关系:`KqRuleArea.java`
  • 后端区域控制器:`KqAfAreaController.java`
  • 后端区域服务:`KqAfAreaServiceImpl.java`
  • PC 区域维护页:`zhkq-web/src/views/attendance/area/AreaModal.vue`
  • PC 地图绘制弹窗:`SelectMapPolygonModal.vue`
  • PC 接口封装:`area.api.ts`
  • PC 表单 Schema:`area.data.ts`
  • 一码多端移动端规则缓存:`src/common/store/clockInfo.js`
  • 移动端多边形判断:`checkPointInPolygon.js`
  • 移动端定位封装:`getLocation.js`

下面的代码不是内部工程原文件全文复制,而是按真实字段、接口风格和业务边界重新整理的一条公开 Demo。它适合迁移到门店签到、工地到场、巡检点位、仓库进出、养老院护理到岗等系统。

一、电子围栏不能只看定位点

定位打卡至少要处理四个问题:

|---------|--------------------|----------------------|
| 问题 | 只做定位接口的后果 | 更稳的设计 |
| 区域不是圆形 | 半径放大后相邻区域也能打卡 | 支持多边形围栏 |
| 规则会变更 | 今天改了区域,明天说不清什么时候生效 | 区域配置走草稿、审核、生效 |
| 人员权限不同 | 一个员工可能绑定不同单位或规则 | 区域与规则单独建关系表 |
| 失败原因不清楚 | 员工只看到"打卡失败" | 返回定位失败、未进围栏、规则未配置等原因 |

真实项目里的 `KqAfArea` 不只是保存点位,它还有 `unitId`、`unitName`、`areaName`、`areaJson`、`areaText`、`afStatus`、`copyId` 等字段。`afStatus` 让区域配置进入流程状态,`copyId` 让已生效区域再次编辑时生成副本,审核通过后再覆盖原记录。这一点非常关键:考勤规则一旦影响工资和管理记录,就不能随便改完立即生效。

`KqRuleArea` 则把规则和区域拆开。一个规则可以绑定多个区域,一个区域也可能被不同规则复用。这样设计比把区域 JSON 直接塞进规则表更稳,因为区域有自己的维护、审批、导入导出和权限过滤。

二、核心字段设计

1. 考勤区域表

|----------------|------------|-----------------------|
| 字段 | 说明 | 为什么需要 |
| `id` | 区域主键 | 规则绑定、审批副本、查询详情都依赖它 |
| `unitId` | 考勤单位 ID | 用于单位维度的数据隔离 |
| `unitName` | 考勤单位名称 | 导入导出和页面展示更友好 |
| `areaName` | 区域名称 | 移动端命中围栏后可显示"总部办公区"等提示 |
| `areaJson` | 多边形坐标 JSON | 保存地图绘制后的边界点 |
| `areaText` | 区域描述 | 给运维和 HR 解释这个区域覆盖哪里 |
| `afStatus` | 审核状态 | 草稿、审核中、完成、驳回 |
| `copyId` | 被编辑原记录 ID | 已生效区域再次编辑时生成副本 |
| `sysOrgCode` | 所属组织 | 做组织权限过滤 |

2. 规则区域关系表

|----------------|---------|--------------|
| 字段 | 说明 | 为什么需要 |
| `ruleId` | 考勤规则 ID | 把区域绑定到具体规则 |
| `areaId` | 考勤区域 ID | 支持一个规则绑定多个区域 |
| `sysOrgCode` | 所属组织 | 避免跨组织误绑定 |

真实项目里,PC 后台的 `AreaModal.vue` 通过"绘制"按钮打开 `SelectMapPolygonModal.vue`。用户在高德地图上新建多边形,结束编辑后把点位数组回传给表单,最终保存为:

复制代码
[

  [114.421832, 30.486012],

  [114.424368, 30.486155],

  [114.424021, 30.483902],

  [114.421517, 30.483766]

]

移动端判断时要注意顺序:多边形点位是 `lng, lat`,定位返回一般是 `longitude`、`latitude`。真实代码里调用方式是:

复制代码
checkPointInPolygon([lnglat.longitude, lnglat.latitude], item.coordinates)

如果把经纬度顺序写反,系统不会报错,但判断结果会完全不可信,这是定位类系统非常隐蔽的坑。

三、完整源码 Demo:多边形电子围栏闭环

1. Entity:考勤区域

复制代码
package com.demo.attendance.entity;



import com.baomidou.mybatisplus.annotation.TableName;

import lombok.Data;



/**

 * 考勤区域配置。

 * 业务边界:负责保存电子围栏基础信息和审批状态,不直接计算员工考勤结果。

 */

@Data

@TableName("demo_kq_af_area")

public class AttendanceArea {

    /** 主键 */

    private String id;

    /** 考勤单位 ID */

    private String unitId;

    /** 考勤单位名称 */

    private String unitName;

    /** 区域名称,例如总部办公区、仓库东门 */

    private String areaName;

    /** 多边形点位 JSON,格式为 [[lng,lat],[lng,lat]] */

    private String areaJson;

    /** 区域描述,用于解释覆盖范围 */

    private String areaText;

    /** 审核状态:-1 草稿,1 审核中,2 已完成,3 驳回 */

    private Integer afStatus;

    /** 已生效区域编辑时生成副本,记录原区域 ID */

    private String copyId;

    /** 所属组织编码 */

    private String sysOrgCode;

}

2. Entity:规则区域关系

复制代码
package com.demo.attendance.entity;



import com.baomidou.mybatisplus.annotation.TableName;

import lombok.Data;



/**

 * 考勤规则与考勤区域关系。

 * 业务边界:只维护规则和区域的绑定关系,不保存区域坐标详情。

 */

@Data

@TableName("demo_kq_rule_area")

public class AttendanceRuleArea {

    /** 主键 */

    private String id;

    /** 考勤规则 ID */

    private String ruleId;

    /** 考勤区域 ID */

    private String areaId;

    /** 所属组织编码 */

    private String sysOrgCode;

}

3. DTO:区域保存请求

复制代码
package com.demo.attendance.dto;



import lombok.Data;

import javax.validation.constraints.NotBlank;

import javax.validation.constraints.Size;



/**

 * 考勤区域保存请求。

 * 业务边界:用于后台新增或编辑电子围栏配置。

 */

@Data

public class AttendanceAreaSaveDTO {

    /** 编辑时传入区域 ID */

    private String id;



    /** 考勤单位 ID */

    @NotBlank(message = "考勤单位不能为空")

    private String unitId;



    /** 考勤单位名称 */

    @NotBlank(message = "考勤单位名称不能为空")

    private String unitName;



    /** 区域名称 */

    @NotBlank(message = "区域名称不能为空")

    @Size(max = 60, message = "区域名称不能超过 60 个字符")

    private String areaName;



    /** 多边形坐标 JSON */

    @NotBlank(message = "请绘制考勤区域")

    @Size(max = 2000, message = "区域坐标过多,请简化边界点")

    private String areaJson;



    /** 区域描述 */

    @NotBlank(message = "区域描述不能为空")

    @Size(max = 600, message = "区域描述不能超过 600 个字符")

    private String areaText;

}

4. VO:移动端可用区域

复制代码
package com.demo.attendance.vo;



import lombok.Data;



/**

 * 移动端考勤区域视图。

 * 业务边界:只返回打卡判断需要的信息,避免把审批字段暴露给移动端。

 */

@Data

public class AppAttendanceAreaVO {

    /** 区域 ID */

    private String id;

    /** 区域名称 */

    private String areaName;

    /** 多边形坐标 JSON */

    private String areaJson;

}

5. Mapper

复制代码
package com.demo.attendance.mapper;



import com.baomidou.mybatisplus.core.mapper.BaseMapper;

import com.demo.attendance.entity.AttendanceArea;

import com.demo.attendance.vo.AppAttendanceAreaVO;

import org.apache.ibatis.annotations.Mapper;

import org.apache.ibatis.annotations.Param;

import org.apache.ibatis.annotations.Select;

import java.util.List;



/**

 * 考勤区域 Mapper。

 * 业务边界:提供后台区域维护查询和移动端规则区域查询。

 */

@Mapper

public interface AttendanceAreaMapper extends BaseMapper<AttendanceArea> {



    /**

     * 查询某个考勤规则下已生效的电子围栏。

     *

     * @param ruleId 考勤规则 ID

     * @return 移动端可用于打卡判断的区域列表

     */

    @Select("select a.id, a.area_name as areaName, a.area_json as areaJson " +

            "from demo_kq_af_area a " +

            "inner join demo_kq_rule_area ra on ra.area_id = a.id " +

            "where ra.rule_id = #{ruleId} and a.af_status = 2")

    List<AppAttendanceAreaVO> selectAppAreasByRuleId(@Param("ruleId") String ruleId);

}

6. Service

复制代码
package com.demo.attendance.service;



import com.baomidou.mybatisplus.extension.service.IService;

import com.demo.attendance.dto.AttendanceAreaSaveDTO;

import com.demo.attendance.entity.AttendanceArea;

import com.demo.attendance.vo.AppAttendanceAreaVO;

import java.util.List;



/**

 * 考勤区域服务。

 * 业务边界:负责区域保存、编辑副本、规则区域查询,不负责移动端定位采集。

 */

public interface AttendanceAreaService extends IService<AttendanceArea> {



    /**

     * 新增考勤区域,默认进入草稿状态。

     *

     * @param dto 区域保存请求

     */

    void addArea(AttendanceAreaSaveDTO dto);



    /**

     * 编辑考勤区域。已审核通过的区域不直接覆盖,而是生成待审核副本。

     *

     * @param dto 区域保存请求

     * @return 编辑结果说明

     */

    String editArea(AttendanceAreaSaveDTO dto);



    /**

     * 查询规则下可用于移动端打卡判断的围栏。

     *

     * @param ruleId 考勤规则 ID

     * @return 区域列表

     */

    List<AppAttendanceAreaVO> listAppAreas(String ruleId);

}

7. ServiceImpl:副本编辑和名称去重

复制代码
package com.demo.attendance.service.impl;



import cn.hutool.core.bean.BeanUtil;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;

import com.demo.attendance.dto.AttendanceAreaSaveDTO;

import com.demo.attendance.entity.AttendanceArea;

import com.demo.attendance.mapper.AttendanceAreaMapper;

import com.demo.attendance.service.AttendanceAreaService;

import com.demo.attendance.vo.AppAttendanceAreaVO;

import lombok.extern.slf4j.Slf4j;

import org.springframework.stereotype.Service;

import org.springframework.transaction.annotation.Transactional;

import java.util.List;



/**

 * 考勤区域服务实现。

 * 业务边界:复刻真实项目中的草稿、已完成、副本编辑思路,保证区域变更可审计。

 */

@Slf4j

@Service

@Transactional(rollbackFor = Exception.class)

public class AttendanceAreaServiceImpl

        extends ServiceImpl<AttendanceAreaMapper, AttendanceArea>

        implements AttendanceAreaService {



    private static final int DRAFT = -1;

    private static final int IN_SERVICE = 1;

    private static final int COMPLETED = 2;



    /**

     * 新增考勤区域。

     * 先做同单位同名校验,再保存草稿,避免移动端立即使用未审核区域。

     */

    @Override

    public void addArea(AttendanceAreaSaveDTO dto) {

        long count = lambdaQuery()

                .eq(AttendanceArea::getUnitId, dto.getUnitId())

                .eq(AttendanceArea::getAreaName, dto.getAreaName())

                .count();

        if (count > 0) {

            throw new IllegalArgumentException("考勤区域名称已存在");

        }

        AttendanceArea area = BeanUtil.toBean(dto, AttendanceArea.class);

        area.setAfStatus(DRAFT);

        save(area);

        log.info("新增考勤区域草稿,unitId={}, areaName={}", dto.getUnitId(), dto.getAreaName());

    }



    /**

     * 编辑考勤区域。

     * 已完成区域不直接覆盖,而是生成副本;审核通过后再覆盖原记录,避免线上规则突然变化。

     */

    @Override

    public String editArea(AttendanceAreaSaveDTO dto) {

        AttendanceArea old = getById(dto.getId());

        if (old == null) {

            throw new IllegalArgumentException("未查询到考勤区域");

        }

        if (IN_SERVICE == old.getAfStatus()) {

            throw new IllegalStateException("正在审核中,不能编辑");

        }

        if (COMPLETED == old.getAfStatus()) {

            AttendanceArea existsCopy = lambdaQuery()

                    .eq(AttendanceArea::getCopyId, old.getId())

                    .one();

            if (existsCopy != null) {

                throw new IllegalStateException("当前区域已有待审核副本,不能重复编辑");

            }

            AttendanceArea copy = BeanUtil.toBean(dto, AttendanceArea.class);

            copy.setId(null);

            copy.setCopyId(old.getId());

            copy.setAfStatus(DRAFT);

            save(copy);

            return "已生成待审核副本,审核通过后覆盖原区域";

        }

        AttendanceArea update = BeanUtil.toBean(dto, AttendanceArea.class);

        updateById(update);

        return "编辑成功";

    }



    /**

     * 查询移动端打卡所需区域。

     * 只返回已审核通过区域,避免草稿或审核中数据影响打卡判断。

     */

    @Override

    public List<AppAttendanceAreaVO> listAppAreas(String ruleId) {

        return baseMapper.selectAppAreasByRuleId(ruleId);

    }

}

8. Controller

复制代码
package com.demo.attendance.controller;



import com.demo.attendance.dto.AttendanceAreaSaveDTO;

import com.demo.attendance.service.AttendanceAreaService;

import com.demo.attendance.vo.AppAttendanceAreaVO;

import lombok.RequiredArgsConstructor;

import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

import java.util.List;



/**

 * 考勤区域接口。

 * 业务边界:后台维护电子围栏,移动端查询已生效围栏。

 */

@RestController

@RequiredArgsConstructor

@RequestMapping("/demo/attendance/area")

public class AttendanceAreaController {



    private final AttendanceAreaService attendanceAreaService;



    /**

     * 新增电子围栏。

     *

     * @param dto 保存请求

     * @return 处理结果

     */

    @PostMapping("/add")

    public String add(@RequestBody @Valid AttendanceAreaSaveDTO dto) {

        attendanceAreaService.addArea(dto);

        return "添加成功";

    }



    /**

     * 编辑电子围栏。

     *

     * @param dto 保存请求

     * @return 处理结果

     */

    @PostMapping("/edit")

    public String edit(@RequestBody @Valid AttendanceAreaSaveDTO dto) {

        return attendanceAreaService.editArea(dto);

    }



    /**

     * 移动端查询当前规则可用电子围栏。

     *

     * @param ruleId 考勤规则 ID

     * @return 围栏列表

     */

    @GetMapping("/app/list")

    public List<AppAttendanceAreaVO> appList(@RequestParam String ruleId) {

        return attendanceAreaService.listAppAreas(ruleId);

    }

}

9. 混合框架移动端:多边形命中判断

复制代码
// common/utils/checkPointInPolygon.js

export function checkPointInPolygon(point, polygon) {

  let intersectCount = 0;

  for (let i = 0, len = polygon.length; i < len; i++) {

    const p1 = polygon[i];

    const p2 = polygon[(i + 1) % len];

    if (point[1] < Math.min(p1[1], p2[1]) || point[1] > Math.max(p1[1], p2[1])) {

      continue;

    }

    if (point[1] === p1[1] && point[1] === p2[1]) {

      continue;

    }

    const x = (point[1] - p1[1]) * (p2[0] - p1[0]) / (p2[1] - p1[1]) + p1[0];

    if (x > point[0]) {

      intersectCount++;

    }

  }

  return intersectCount % 2 === 1;

}

真实项目里的移动端规则缓存大致是这个思路:

复制代码
async function buildAreaCoordinates(ruleId) {

  const areas = await http.get('/demo/attendance/area/app/list', { ruleId });

  return areas

    .filter(item => item.areaJson)

    .map(item => ({

      areaId: item.id,

      areaName: item.areaName,

      coordinates: JSON.parse(item.areaJson)

    }));

}



async function checkLocationInArea(ruleId) {

  const areaCoordinates = await buildAreaCoordinates(ruleId);

  const lnglat = await getCurrentLocation();



  const matched = areaCoordinates.find(item => {

    return checkPointInPolygon([lnglat.longitude, lnglat.latitude], item.coordinates);

  });



  if (!matched) {

    return Promise.reject({

      message: '当前位置不在考勤区域内',

      longitude: lnglat.longitude,

      latitude: lnglat.latitude

    });

  }



  return {

    type: 'areaCoordinate',

    areaId: matched.areaId,

    address: matched.areaName,

    longitude: lnglat.longitude,

    latitude: lnglat.latitude

  };

}

10. MySQL 表结构示例

复制代码
CREATE TABLE `demo_kq_af_area` (

  `id` varchar(32) NOT NULL DEFAULT '' COMMENT '主键',

  `unit_id` varchar(32) NOT NULL DEFAULT '' COMMENT '考勤单位ID',

  `unit_name` varchar(100) NOT NULL DEFAULT '' COMMENT '考勤单位名称',

  `area_name` varchar(100) NOT NULL DEFAULT '' COMMENT '区域名称',

  `area_json` varchar(2000) NOT NULL DEFAULT '' COMMENT '区域多边形JSON',

  `area_text` varchar(600) NOT NULL DEFAULT '' COMMENT '区域描述',

  `af_status` int(11) NOT NULL DEFAULT -1 COMMENT '审核状态:-1草稿,1审核中,2已完成,3驳回',

  `copy_id` varchar(32) NOT NULL DEFAULT '' COMMENT '副本来源ID',

  `sys_org_code` varchar(64) NOT NULL DEFAULT '' COMMENT '所属组织',

  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',

  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',

  PRIMARY KEY (`id`),

  KEY `idx_unit_area` (`unit_id`, `area_name`),

  KEY `idx_af_status` (`af_status`),

  KEY `idx_copy_id` (`copy_id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='考勤区域表';



CREATE TABLE `demo_kq_rule_area` (

  `id` varchar(32) NOT NULL DEFAULT '' COMMENT '主键',

  `rule_id` varchar(32) NOT NULL DEFAULT '' COMMENT '考勤规则ID',

  `area_id` varchar(32) NOT NULL DEFAULT '' COMMENT '考勤区域ID',

  `sys_org_code` varchar(64) NOT NULL DEFAULT '' COMMENT '所属组织',

  PRIMARY KEY (`id`),

  KEY `idx_rule_id` (`rule_id`),

  KEY `idx_area_id` (`area_id`),

  KEY `idx_rule_area` (`rule_id`, `area_id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='考勤规则区域关系表';

四、关键代码为什么这样设计

1. 区域 JSON 不直接塞进规则表

如果把 `areaJson` 直接放进考勤规则表,维护起来最省事,但后续会遇到三个麻烦:

第一,多个规则复用同一个区域时会重复保存。第二,区域变更后无法知道哪些规则被影响。第三,区域审批、生效和导出会和规则表混在一起。真实项目把 `KqAfArea` 和 `KqRuleArea` 拆开,虽然表多了一张,但边界清楚:区域负责地图边界,规则负责什么时候、哪些人、哪些班次生效。

2. 已生效区域编辑要生成副本

`KqAfAreaServiceImpl.edit` 里有个很好的工程细节:如果老区域已经审核完成,编辑时不会直接覆盖,而是生成一条 `copyId=原区域ID` 的草稿记录。审核通过后再把副本数据覆盖回原区域,并删除副本。这种设计适合所有会影响薪资、绩效、审批结果的配置。

否则今天 10 点 HR 改了区域,员工 9 点的异常到底按旧区域还是新区域算?没有副本和审批记录,系统很难解释。

3. 移动端先做本地判断,但后端仍要留证据

移动端用 `checkPointInPolygon` 做快速判断,体验很好;但正式打卡记录里仍然应该保存 `longitude`、`latitude`、`areaName`、`locationType`、`address`、`clockTime` 等字段。原因是移动端判断只是"当下能不能提交",后续异常申诉、主管审核、报表统计需要能追溯当时的证据。

4. 多边形点位要限制复杂度

PC 后台表单里对 `areaJson` 做了长度限制。这个限制看似粗糙,但很实用。地图上如果画出过多点位,前端解析、移动端缓存、网络传输都会增加成本。企业考勤不是 GIS 专业系统,围栏点位要足够表达边界,但不能追求地图级精度。

五、容易踩坑的地方

|----------|-----------------|--------------------------|
| 坑 | 表现 | 建议 |
| 经纬度顺序写反 | 判断结果随机,看起来像定位漂移 | 统一约定点位为 `lng, lat` |
| 只做半径判断 | 办公楼、园区、仓库容易误判 | 复杂场景用多边形围栏 |
| 区域修改立即生效 | 历史异常无法解释 | 已生效配置走副本和审批 |
| 移动端只提示失败 | 员工不知道怎么处理 | 返回未进围栏、定位失败、规则未配置等原因 |
| 围栏点位过多 | 移动端缓存和判断变慢 | 限制点位数量或 JSON 长度 |
| 只相信前端判断 | 数据容易被篡改或无法追溯 | 打卡记录保存原始经纬度和命中类型 |
| 地图坐标系混用 | 高德、GPS 原始坐标偏移 | 统一使用 `gcj02` 或做坐标转换 |

六、这套设计可以怎么复用

电子围栏打卡不是考勤系统专属。只要业务里有"人、车、设备是否到达某个范围"的判断,都可以复用这套设计:

  • 门店签到:店员是否在门店范围内开店、闭店。
  • 仓库巡检:巡检人员是否到达库区或设备区。
  • 工地到场:班组长是否到达施工区域。
  • 养老院护理:护理员是否到达楼层或护理区。
  • 外勤工单:维修人员是否到达客户现场。

可复用的不是 `checkPointInPolygon` 这个函数本身,而是整条链路:后台画围栏、配置走审批、规则绑定区域、移动端做命中判断、记录保存证据、异常可以申诉和追溯。

七、总结

定位打卡做不好,很多时候不是定位接口不准,而是系统没有把"范围"当成一个需要建模、审批和解释的业务对象。半径判断能解决第一版上线,但多部门、多区域、多规则、多角色协同时,必须把电子围栏单独抽出来。

一个相对可靠的方案应该至少做到:

  1. PC 后台支持地图绘制多边形区域。

  2. 区域配置独立建表,并保留单位、名称、描述和审批状态。

  3. 考勤规则通过关系表绑定区域,不把区域 JSON 写死到规则里。

  4. 移动端统一用 `longitude, latitude` 判断多边形命中。

  5. 打卡记录保存原始经纬度、命中区域和失败原因。

  6. 已生效区域变更要生成副本并走审核。

这样做不是为了把考勤管得更死,而是让系统在出现争议时能说清楚:当时规则是什么、围栏在哪里、员工的位置是什么、为什么判定成功或失败。

相关推荐
智联物联1 个月前
办公楼转型养老公寓,边缘计算网关实现全场景智慧监护
人工智能·边缘计算·物联网解决方案·工业网关·智慧养老·数采网关·边缘盒子
寺中人2 个月前
基于毫米波雷达监测系统的硬件选型与组装调试
人工智能·智能家居·工具·养老·智慧养老·5gcpe
青云交8 个月前
Java 大视界 -- Java 大数据在智慧养老服务需求分析与个性化服务匹配中的应用
java·需求分析·智慧养老·健康管理·java 大数据·个性化服务·生活照料
凯禾瑞华养老实训室9 个月前
聚焦生活照护能力培育:老年生活照护实训室建设清单的模块设计与资源整合
大数据·人工智能·科技·ar·vr·智慧养老·智慧健康养老服务与管理
招风的黑耳9 个月前
智慧养老管理系统原型设计:医疗级后台与全场景交互方案
智慧养老·axure后台模板
编程迪2 年前
智慧养老系统源码医院陪诊代办买药就医陪护上门护理小程序
小程序·智慧养老·就医陪诊·医院陪护·养老系统源码
武汉唯众智创2 年前
基于“互联网+”医养结合的智慧养老实训室建设方案
智慧养老·互联网+·医养结合
武汉唯众智创2 年前
机构养老实训室解决方案
智慧养老·机构养老实训室·机构养老
郑洁文2 年前
养老院生活管理系统
毕业设计·毕设·养老院管理系统·养老院生活管理系统·养老院系统