# 考勤打卡规则组接口文档

本文档覆盖 HRO 管理后台打卡规则组相关接口，对应页面：

```text
/dhr/attendancePro/admin/pc/home.html#/punchRuleList
```

## 基本信息

本地服务地址：

```text
http://127.0.0.1:8080
```

除 `/api/auth/login` 外，接口需要携带 Bearer Token：

```http
Authorization: Bearer <token>
```

打卡规则组和打卡资源支持 fixture/JDBC 双 store；是否持久化取决于对应 `hro.attendance.*.persistence.enabled` 开关。

## 项目隔离

- 当前项目来自管理端 IAM token 中的 projectId；平台超管传入 `X-Project-Id` 时，后端会把它作为本次有效 projectId。
- projectId 非空时，规则组、考勤地址、WIFI、考勤机、考勤机员工关系、考勤区域、人脸识别配置只读写当前项目数据；新建记录写入当前 projectId。
- projectId 为空时视为平台全局态，不加项目过滤；新建记录的 `project_id` 为 `NULL`。
- `project_id = NULL` 表示平台全局或历史未归属记录，项目态默认不可见。若需要项目默认数据，需要按项目初始化或复制，不会隐式继承历史全局记录。
- 保存规则组时，`addressIdList`、`wifiIdList`、`deviceIdList` 引用的资源必须在当前项目可见；跨项目或不存在资源会返回业务错误（HTTP 200，`code` 非 0）。考勤机员工关系也不能绑定不存在或其它项目的考勤机。
- 员工端打卡运行时从员工 token 取 projectId，后端按该 projectId 读取规则组、WIFI 和考勤地址；规则已配置但资源不可见时不会降级放行，而是返回「配置不可用」类业务错误。

## 通用响应

EHR 旧接口响应：

```json
{
  "code": 0,
  "costTimes": 0,
  "data": {},
  "message": "请求成功"
}
```

## 公共接口（复用已有模块）

以下接口由登录鉴权、加班规则、请假规则模块提供，打卡规则组页面可复用：

| 接口 | 说明 |
|------|------|
| `GET /n/oursContext?routerUrl=...` | 页面上下文 |
| `GET /home.do?method=getPortalColor` | 主题色 |
| `POST /dhr/admin/atd/common/generateCode` | 生成编码 |
| `GET /dhr/user/selectPerson/init` | 组织选择初始化 |
| `POST /dhr/user/selectPerson/getOrg` | 获取组织树 |

公共接口文档详见 [登录鉴权](../auth/api.md)、[加班规则](overtime-api.md)、[请假规则](leave-api.md)。

---

## 打卡规则组 / checkTemplate

### POST `/dhr/admin/atd/checkTemplate/index`

获取打卡规则组编辑页面初始化数据。

请求体可为空 `{}`。

响应：

```json
{
  "code": 0,
  "data": {
    "prefix": "AE",
    "addressShowTypes": [
      { "id": "location", "value": "location", "display": "位置" },
      { "id": "wifi", "value": "wifi", "display": "WIFI" },
      { "id": "device", "value": "device", "display": "考勤机" }
    ],
    "aiFaceOptions": [
      { "id": 0, "value": 0, "display": "关闭" },
      { "id": 1, "value": 1, "display": "开启" }
    ],
    "dateTypes": [
      { "id": "workDay", "value": "workDay", "display": "工作日", "type": "workDay", "disabled": false },
      { "id": "restDay", "value": "restDay", "display": "休息日", "type": "restDay", "disabled": false },
      { "id": "holiday", "value": "holiday", "display": "节假日", "type": "holiday", "disabled": false }
    ],
    "rightTypes": [
      { "id": 0, "value": 0, "display": "按管理范围" },
      { "id": 1, "value": 1, "display": "按人员" }
    ],
    "outsideOptions": [
      { "id": 0, "value": 0, "display": "关闭" },
      { "id": 1, "value": 1, "display": "开启" }
    ]
  }
}
```

### POST `/dhr/admin/atd/checkTemplate/list`

分页查询打卡规则组。

请求：

```json
{
  "pageIndex": "1",
  "pageSize": "20",
  "queryParam": [
    {
      "expression": "in",
      "field": "state",
      "value": "0"
    }
  ],
  "sortFields": []
}
```

响应：

```json
{
  "code": 0,
  "data": {
    "dataList": [
      {
        "id": "4614657879024617305",
        "code": "4614657879024617305",
        "prefix": "AE",
        "name": "打卡",
        "mgrOrgId": "9017397430310779032",
        "mgrOrgName": "新乐市中医医院",
        "mgrOrgType": "department",
        "sourceType": "预置",
        "state": 0,
        "description": "",
        "addressShowType": "location",
        "aiFace": 0,
        "allDayOfficial": 0,
        "leaveCheckNoWork": 0,
        "notAllDayLeave": 0,
        "notAllDayOfficial": 0,
        "officialCheckNoWork": 0,
        "outside": 1,
        "outsideCheckRangeAfter": 0,
        "outsideCheckRangeBefore": 0,
        "outsideEarly": "",
        "outsideLate": "",
        "outsideNeedOfficial": 0,
        "outsideNeedOther": 0,
        "outsideNeedRemark": 1,
        "outsidePhoto": 0,
        "startTimeDay": "00:00",
        "rightType": 0
      }
    ],
    "needTotal": true,
    "pageIndex": 1,
    "pageSize": 20,
    "queryParam": [],
    "sortFields": [],
    "total": 1
  }
}
```

支持按 `state`（精确匹配）和 `name`（模糊匹配）筛选。

### POST `/dhr/admin/atd/checkTemplate/save`

新建打卡规则组。

请求：

```json
{
  "addressIdList": ["6734019960757770354"],
  "wifiIdList": [],
  "deviceIdList": [],
  "checkTemplateGroup": {
    "code": "编码（可选，为空自动生成）",
    "name": "规则名称",
    "prefix": "AE",
    "mgrOrg": [
      {
        "id": "9017397430310779032",
        "name": "新乐市中医医院",
        "typeFlag": "department"
      }
    ],
    "mgrOrgId": "9017397430310779032",
    "mgrOrgName": "新乐市中医医院",
    "mgrOrgType": "department",
    "state": 0,
    "description": "",
    "addressShowType": "location",
    "aiFace": 0,
    "allDayOfficial": 0,
    "leaveCheckNoWork": 0,
    "notAllDayLeave": 0,
    "notAllDayLeaveRangeStart": null,
    "notAllDayLeaveRangeEnd": null,
    "notAllDayOfficial": 0,
    "notAllDayOfficialRangeStart": null,
    "notAllDayOfficialRangeEnd": null,
    "officialCheckNoWork": 0,
    "outside": 1,
    "outsideCheckRangeBefore": 0,
    "outsideCheckRangeAfter": 0,
    "outsideEarly": "",
    "outsideLate": "",
    "outsideNeedOfficial": 0,
    "outsideNeedOther": 0,
    "outsideNeedRemark": 1,
    "outsidePhoto": 0,
    "startTimeDay": "00:00",
    "rightType": 0
  }
}
```

- `addressIdList`、`wifiIdList`、`deviceIdList` 为顶层字段，保存后关联存储。**至少需要提供一个非空列表**，否则返回业务错误（`code` 非 0）。
- `addressIdList`、`wifiIdList`、`deviceIdList` 中的 id 必须属于当前有效项目；跨项目资源会分别返回「考勤地址不存在或已删除」「WIFI不存在或已删除」「考勤机不存在或已删除」。
- `checkTemplateGroup` 为嵌套对象。
- **外勤打卡与公出不冲抵互斥**：`outside`（外勤打卡）与 `officialCheckNoWork`（公出不冲抵打卡）不可同时设为 `1`，保存时校验互斥关系。
- **海外打卡字段**：规则组支持保存海外打卡相关配置（海外地点、城市、时区、跨天打卡开始时间），当前阶段保存回显，不实现跨时区核算引擎。
- **人脸识别配置**：`aiFace` 开启后可配置底照信息，底照记录由 `aiFace/save|update|delete` 接口管理。
- 新建的规则组 `sourceType` 为"自定义"，更新时保留原有 `sourceType`（除非请求显式覆盖）。

响应返回保存后的完整 `checkTemplateGroup` 记录。

### GET `/dhr/admin/atd/checkTemplate/detail?id=...`

查询打卡规则组详情，同时返回关联资源。

响应：

```json
{
  "code": 0,
  "data": {
    "checkTemplateGroup": {
      "id": "4614657879024617305",
      "code": "4614657879024617305",
      "prefix": "AE",
      "name": "打卡",
      "mgrOrgId": "9017397430310779032",
      "mgrOrgName": "新乐市中医医院",
      "mgrOrgType": "department",
      "sourceType": "预置",
      "state": 0,
      "description": "",
      "addressShowType": "location",
      "aiFace": 0,
      "outside": 1,
      "outsideCheckRangeBefore": 0,
      "outsideCheckRangeAfter": 0,
      "startTimeDay": "00:00",
      "rightType": 0
    },
    "addressList": [
      {
        "id": "6734019960757770354",
        "code": "6734019960757770354",
        "prefix": "AC",
        "name": "石家庄市人民政府",
        "detailAddress": "河北省石家庄市长安区中山东路216号",
        "range": 50,
        "longitude": 114.515043,
        "latitude": 38.042067,
        "state": 0
      }
    ],
    "wifiList": [],
    "deviceList": []
  }
}
```

### POST `/dhr/admin/atd/checkTemplate/update`

更新打卡规则组，请求格式与 save 一致。行为和 save 相同（幂等保存），同样受资源必填、外勤/公出不冲抵互斥等校验约束。

项目态只允许更新当前项目可见的规则组；跨项目 id 按不存在处理。

### POST `/dhr/admin/atd/checkTemplate/delete`

删除打卡规则组（同时清除关联资源映射）。被考勤方案引用的打卡规则组不可删除，删除前会检查方案引用关系。

项目态只允许删除当前项目可见的规则组；跨项目 id 按不存在处理。

> **阶段边界**：考勤方案引用删除保护由 fixture 层模拟，真实业务中还需要校验更多依赖。

请求：

```json
{
  "ids": ["4614657879024617305"]
}
```

`eids` 字段会被忽略。响应 `data` 为 `true`。

---

## 考勤地址 / address

### POST `/dhr/admin/atd/address/index`

获取考勤地址初始化数据。

请求体可为空 `{}`。

响应：

```json
{
  "code": 0,
  "data": {
    "prefix": "AC",
    "regions": []
  }
}
```

### POST `/dhr/admin/atd/address/list`

分页查询考勤地址。默认包含一条记录：

| 字段 | 值 |
|------|----|
| code | 6734019960757770354 |
| prefix | AC |
| 业务编码 | AC6734019960757770354 |
| name | 石家庄市人民政府 |
| detailAddress | 河北省石家庄市长安区中山东路216号 |
| range | 50 |
| longitude | 114.515043 |
| latitude | 38.042067 |
| state | 0 |
| regionName | （空字符串） |

### POST `/dhr/admin/atd/address/save`

新增考勤地址。

请求：

```json
{
  "code": "编码（可选，为空自动生成）",
  "detailAddress": "详细地址",
  "latitude": "纬度（字符串形式，如 \"12\"）",
  "longitude": "经度（字符串形式，如 \"123\"）",
  "name": "地址名称",
  "prefix": "AC",
  "range": 100
}
```

响应返回保存后的完整地址记录，至少包含 `code`、`detailAddress`、`id`、`latitude`、`longitude`、`name`、`prefix`、`range`、`state` 字段。

### POST `/dhr/admin/atd/address/update`

更新考勤地址。按 `id` 覆盖已有行；如果 `id` 不存在，按保存逻辑插入。

- **`code` 字段不可修改**：更新时传入的 `code` 值会被忽略，保留创建时的编码。若编码有误需删除后重新添加。
- 项目态只允许更新当前项目可见的地址；跨项目 id 按不存在处理。

请求：

```json
{
  "id": "target-address-id",
  "code": "编码（不会被更新，保留原值）",
  "detailAddress": "详细地址",
  "latitude": "纬度",
  "longitude": "经度",
  "name": "地址名称",
  "prefix": "AC",
  "range": 100,
  "regionName": "区域名称",
  "state": 0
}
```

响应返回更新后的完整地址记录。

### POST `/dhr/admin/atd/address/delete`

删除考勤地址。被打卡规则组引用的地址不允许删除，删除前会检查引用关系，若存在引用则返回业务错误。

请求：

```json
{
  "eids": [null],
  "ids": ["address-id-1", "address-id-2"]
}
```

- `eids` 字段会被忽略。
- 按 `ids` 列表删除对应地址，未被引用的地址正常删除。
- 若地址已被任一当前项目可见规则组引用，删除会被拒绝。

响应 `data` 为 `true`。

---

## WIFI / wifi

### POST `/dhr/admin/atd/wifi/index`

获取 WIFI 初始化数据。

请求体可为空 `{}`。

响应：

```json
{
  "code": 0,
  "data": {
    "prefix": "AA",
    "regions": []
  }
}
```

### POST `/dhr/admin/atd/wifi/list`

分页查询 WIFI，初始为空列表。

### POST `/dhr/admin/atd/wifi/save`

新增 WIFI。

请求：

```json
{
  "code": "编码（可选）",
  "prefix": "AA",
  "wifiMac": "00:11:22:33:44:55",
  "wifiSsid": "Office-WiFi"
}
```

响应返回 `id`、`code`、`prefix`、`name`（默认等于 `wifiSsid`）、`wifiSsid`、`wifiMac`、`state`、`regionName`。

### POST `/dhr/admin/atd/wifi/update`

更新 WIFI，按 `id` 覆盖已有行；如果 `id` 不存在，按保存逻辑插入。

- **`code` 字段不可修改**：更新时传入的 `code` 值会被忽略，保留创建时的编码。`wifiMac` 和 `wifiSsid` 可以正常修改。若编码有误需删除后重新添加。

请求：

```json
{
  "id": "wifi-id",
  "code": "编码",
  "name": "123",
  "prefix": "AA",
  "regionName": "",
  "state": 0,
  "wifiMac": "14:10:18:55:A9:7b",
  "wifiSsid": "123"
}
```

响应返回 `id`、`code`、`prefix`、`name`、`wifiSsid`、`wifiMac`、`state`、`regionName`。

### POST `/dhr/admin/atd/wifi/delete`

删除 WIFI。被打卡规则组引用的 WIFI 不允许删除，删除前会检查引用关系，若存在引用则返回业务错误。

请求：

```json
{
  "eids": [null],
  "ids": ["wifi-id"]
}
```

- `eids` 字段会被忽略。
- 按 `ids` 列表删除对应 WIFI，未被引用的正常删除。
- 若 WIFI 已被任一当前项目可见规则组引用，删除会被拒绝。

响应 `data` 为 `true`。

---

## 考勤机 / device

### POST `/dhr/admin/atd/device/index`

获取考勤机初始化数据。

请求体可为空 `{}`。

响应：

```json
{
  "code": 0,
  "data": {
    "prefix": "AB",
    "regions": [],
    "deviceBrandEnum": [
      { "id": "zk", "value": "zk", "display": "中控" }
    ]
  }
}
```

### POST `/dhr/admin/atd/device/list`

分页查询考勤机，初始为空列表。

### POST `/dhr/admin/atd/device/save`

新增考勤机。

请求：

```json
{
  "brand": "zk",
  "brandName": "中控",
  "code": "编码（可选）",
  "description": "备注",
  "display": "前台考勤机",
  "model": "M300",
  "name": "考勤机名称",
  "prefix": "AB",
  "primaryAdapter": "eth0",
  "regionId": "",
  "sn": "SN12345678"
}
```

响应返回 `id`、`code`、`prefix`、`name`、`brand`、`brandName`、`display`、`model`、`primaryAdapter`、`sn`、`state`、`stateName4XT`、`regionName`、`description`、`regionId`。

### POST `/dhr/admin/atd/device/update`

更新考勤机。按 `id` 覆盖已有行；如果 `id` 不存在，按保存逻辑插入。

请求：

```json
{
  "id": "device-id",
  "brand": "zk",
  "brandName": "中控",
  "code": "编码",
  "description": "备注",
  "display": "前台考勤机",
  "model": "M300",
  "name": "考勤机名称",
  "prefix": "AB",
  "primaryAdapter": "eth0",
  "regionId": "",
  "sn": "SN12345678"
}
```

- **`sn` 字段不可修改**：`sn`（序列号）为考勤机硬件唯一标识，更新时传入的 `sn` 值会被忽略，保留创建时的序列号。若 SN 有误需删除后重新添加。
- 其他字段按请求覆盖。

响应返回更新后的完整记录，包含 `id`、`code`、`prefix`、`name`、`brand`、`brandName`、`display`、`model`、`primaryAdapter`、`sn`、`state`、`stateName4XT`、`regionName`、`description`、`regionId`。

### POST `/dhr/admin/atd/device/delete`

删除考勤机。被打卡规则组引用的考勤机不允许删除，删除前会检查引用关系，若存在引用则返回业务错误。

请求：

```json
{
  "ids": ["device-id"]
}
```

- 删除未引用的考勤机时，同步清理该设备的所有人员下发关系。
- 若考勤机已被任一当前项目可见规则组引用，删除会被拒绝。

响应 `data` 为 `true`。

---

## 人员选择 / selectPerson/getPersons

### POST `/dhr/user/selectPerson/getPersons`

根据组织节点查询人员列表，仅 `生产2部` 和 `采购部` 返回人员。

请求：

```json
{
  "companyId": "9017397430310779028",
  "id": "9017397430310779106",
  "includeChildren": false,
  "includeLeave": false,
  "includePartTime": false,
  "roleType": "",
  "type": "org"
}
```

响应（生产2部）：

```json
{
  "code": 0,
  "data": [
    {
      "depLeader": "",
      "deptName": "生产2部",
      "deptNamePath": "河北瑞鹤医疗器械有限公司/瑞鹤医疗测试/生产2部",
      "employeeNumber": "001",
      "headImgUrl": "",
      "id": "2000000000000000001",
      "name": "李兜兜",
      "number": "001",
      "orgId": "9017397430310779106",
      "orgName": "生产2部",
      "outerPerson": false,
      "phone": "",
      "post": "",
      "registerState": 0,
      "registerStateName": "在职",
      "state": 0,
      "typeFlag": 0
    }
  ]
}
```

| 组织节点 | 返回人员 |
|----------|----------|
| 生产2部 | 李兜兜 (2000000000000000001) |
| 采购部 | 李益生 (2000000000000000002) |
| 其他部门 | 空列表 |

组织树已包含以下部门节点（父节点为瑞鹤医疗测试）：

党办室、办公室、财务部门、人事部门、销售部门、生产2部、生产1部、采购部

---

## 考勤机员工关系 / deviceRelation

### POST `/dhr/admin/atd/deviceRelation/index`

获取考勤机员工关系初始化数据，当前返回空对象。

请求体可为空 `{}`。

响应：

```json
{
  "code": 0,
  "data": {}
}
```

### POST `/dhr/admin/atd/deviceRelation/list`

分页查询考勤机员工下发关系。

请求：

```json
{
  "deviceId": "device-id",
  "pageIndex": "1",
  "pageSize": "20",
  "queryParam": [],
  "sortFields": []
}
```

初始数据为空。调用 send 后可返回已分配的关系。

响应字段：

| 字段 | 说明 |
|------|------|
| id | 关系 ID |
| personId | 人员 ID |
| personName / name | 姓名 |
| deptName | 所属部门 |
| deptNamePath | 部门路径 |
| employeeNumber | 工号 |
| number | 编号 |
| deviceId | 考勤机 ID |
| deviceName | 考勤机名称 |
| deviceSn | 考勤机序列号 |
| state | 状态 |
| stateName | 状态名称（正常） |
| sendTime | 下发时间 |
| createTime | 创建时间 |

### POST `/dhr/admin/atd/deviceRelation/send`

批量下发员工到考勤机（单行"分配员工"和批量分配共用此接口）。

请求：

```json
{
  "deviceIds": ["device-id-1"],
  "personRange": [
    {
      "toId": "2000000000000000001",
      "toName": "李兜兜",
      "toType": "person"
    }
  ]
}
```

- `deviceIds`：目标考勤机 ID 列表
- `personRange`：分配人员列表

重复发送同一对 device+person 不会重复追加。

响应 `data` 为 `true`。

### POST `/dhr/admin/atd/deviceRelation/delete`

移除考勤机员工关系。同时支持别名路径 `POST /dhr/admin/atd/deviceRelation/remove`。

请求：

```json
{
  "ids": ["relation-id-1", "relation-id-2"]
}
```

- 按 `ids` 列表删除对应的人员关系记录。
- 删除后该人员不再与该考勤机关联。

> **阶段边界**：离职人员从考勤机移除由离职事件自动触发（离职日期为未来日期则在当日凌晨执行，为过去日期则在离职操作时触发）。fixture 阶段不实现离职事件监听。

响应 `data` 为 `true`。

---

## 考勤区域 / region

### POST `/dhr/admin/atd/region/index`

获取考勤区域初始化数据。

请求体可为空 `{}`。

响应：

```json
{
  "code": 0,
  "data": {
    "prefix": "AD"
  }
}
```

### POST `/dhr/admin/atd/region/list`

分页查询考勤区域，初始为空列表。

请求：

```json
{
  "pageIndex": "1",
  "pageSize": "20",
  "queryParam": [],
  "sortFields": []
}
```

行字段：`id`, `code`, `name`, `prefix`, `state`, `description`。

支持按 `name`、`code` 等字段模糊筛选。

### POST `/dhr/admin/atd/region/save`

新增考勤区域。

请求：

```json
{
  "code": "AD001",
  "name": "区域名称",
  "prefix": "AD",
  "description": "备注（可选）"
}
```

响应返回 `id`、`code`、`name`、`prefix`、`state`、`description`。

### POST `/dhr/admin/atd/region/update`

更新考勤区域，按 `id` 覆盖已有行。

请求：

```json
{
  "id": "region-id",
  "code": "AD001",
  "name": "区域名称",
  "prefix": "AD",
  "state": 0,
  "description": "更新后的备注"
}
```

### POST `/dhr/admin/atd/region/delete`

删除考勤区域，同时清理 addresses/wifis/devices 中引用的 regionId/regionName。

请求：

```json
{
  "eids": [null],
  "ids": ["region-id"]
}
```

- `eids` 字段会被忽略。
- 按 `ids` 列表删除对应区域。

响应 `data` 为 `true`。

---

## 人脸识别 / aiFace

### POST `/dhr/admin/atd/aiFace/index`

获取人脸识别初始化数据。

请求体可为空 `{}`。

响应返回 `prefix` 和基础选项配置。

### POST `/dhr/admin/atd/aiFace/list`

分页查询 AI 人脸识别底库数据。

请求：

```json
{
  "pageIndex": "1",
  "pageSize": "20",
  "queryParam": [],
  "sortFields": []
}
```

初始预置 4 条记录：

| 姓名 | 部门 | 手机号 | 身份证号 |
|------|------|--------|----------|
| 李璨 | 人事部门 | 18333185329 | 请填写 |
| 李真真 | 人事部门 | 15900232432 | 130111200001096666 |
| 李兜兜 | 生产2部 | 11211311411 | 1121131999080899999 |
| 李益生 | 采购部 | 18100201394 | 132132198010140001 |

行字段：`id`, `name`, `deptName`, `orgName`, `phone`, `idNumber`, `faceInfo`, `aiFace`, `inputTime`, `registerTime`。

支持按 `name`、`deptName`、`idNumber` 等字段模糊筛选。

### POST `/dhr/admin/atd/aiFace/save`

新增人脸底库记录（首次录入即存底照）。

请求：

```json
{
  "name": "姓名",
  "deptName": "部门",
  "orgName": "组织",
  "phone": "手机号",
  "idNumber": "身份证号",
  "aiFace": 1
}
```

响应返回完整记录，包含 `id`、`name`、`deptName`、`orgName`、`phone`、`idNumber`、`faceInfo`、`aiFace`、`inputTime`、`registerTime`。

### POST `/dhr/admin/atd/aiFace/update`

更新人脸底库记录，按 `id` 覆盖已有行。

请求：

```json
{
  "id": "record-id",
  "name": "姓名",
  "deptName": "部门",
  "orgName": "组织",
  "phone": "手机号",
  "idNumber": "身份证号",
  "aiFace": 1
}
```

响应返回更新后的完整记录。

### POST `/dhr/admin/atd/aiFace/delete`

删除人脸底库记录。

请求：

```json
{
  "ids": ["record-id-1", "record-id-2"]
}
```

- 按 `ids` 列表删除对应底库记录。

响应 `data` 为 `true`。

> **阶段边界**：人脸验证依赖客户端拍照能力和云端供应商（阿里云/华为云）进行人脸比对。fixture 阶段只保存配置和底库记录，不调用云端人脸供应商。

---

## 默认数据

服务启动时预置：

| 资源 | code | prefix | 业务编码 | name | 所属组织 |
|------|------|--------|----------|------|----------|
| 打卡规则组 | 4614657879024617305 | AE | AE4614657879024617305 | 打卡 | 新乐市中医医院 |
| 考勤地址 | 6734019960757770354 | AC | AC6734019960757770354 | 石家庄市人民政府 | - |

---

## 飞书规则补充

本节根据飞书 Wiki「打卡规则」正文和 27 张截图，补充打卡规则相关后端行为说明。以下规则已由 fixture 层实现校验和保存回显。

### 打卡资源必填校验

保存打卡规则组时，`addressIdList`、`wifiIdList`、`deviceIdList` 至少需要提供一个非空列表。三个列表全部为空（或全部不传）时，返回业务错误（`code` 非 0），提示"至少配置一种打卡方式"。

### 外勤打卡与公出不冲抵互斥

`outside`（外勤打卡开关）与 `officialCheckNoWork`（公出不冲抵打卡开关）互斥，两者不可同时设为 `1`。保存时校验此互斥关系：

- 外勤打卡开启 + 公出不冲抵关闭：允许外勤打卡，公出默认走冲抵逻辑。
- 公出不冲抵开启 + 外勤打卡关闭：公出需按班次时间打卡。
- 两者同时开启：返回业务错误。

### 海外打卡字段保存

打卡规则组保存/更新时支持透传海外打卡相关字段：
- 海外地点、城市、时区配置
- 允许打卡范围
- 跨天打卡开始时间（`startTimeDay`）

> **阶段边界**：当前 fixture 只保存回显这些字段，不实现跨时区打卡核算引擎。真实跨时区打卡需要班次时间配合时差调整、跨天线和海外 license 校验。

### 人脸识别配置与底照管理

`aiFace` 字段控制是否开启人脸识别（`0` 关闭 / `1` 开启）。开启后可通过 `aiFace/save` 录入人脸底照，通过 `aiFace/update` 更新底照信息，通过 `aiFace/delete` 删除底照记录。

> **阶段边界**：人脸验证依赖客户端拍照能力和云端供应商（阿里云/华为云）进行人脸比对。fixture 阶段只保存配置和底库记录，不调用云端人脸供应商，不实现自动人脸识别（需 `product.autoMediaFace=true` 配置文件支持）。

### 引用删除保护

被打卡规则组引用的考勤地址、WIFI、考勤机不允许直接删除。删除操作会先检查以下引用关系：

| 被删除资源 | 检查的引用字段 | 存在引用时的行为 |
|-----------|---------------|-----------------|
| 考勤地址 | 任意打卡规则组的 `addressIdList` | 返回业务错误，拒绝删除 |
| WIFI | 任意打卡规则组的 `wifiIdList` | 返回业务错误，拒绝删除 |
| 考勤机 | 任意打卡规则组的 `deviceIdList` | 返回业务错误，拒绝删除 |
| 打卡规则组 | 考勤方案的 `checkTemplateGroupId` | 返回业务错误，拒绝删除 |

未被引用的资源正常删除；删除考勤机时同步清理该设备的人员下发关系。规则组仍在引用的资源会被拒绝删除，不做隐式解绑。

### 不可修改字段

| 资源 | 不可修改字段 | 说明 |
|------|-------------|------|
| 考勤地址 | `code` | 编码为唯一标识，更新时传入的新值会被忽略，保留创建时的值。若编码有误需删除后重新添加。 |
| WIFI | `code` | 编码为唯一标识，更新时传入的新值会被忽略。`wifiMac` 和 `wifiSsid` 可以正常修改。若编码有误需删除后重新添加。 |
| 考勤机 | `sn` | 序列号为考勤机硬件唯一标识，更新时传入的新值会被忽略，保留创建时的值。若 SN 有误需删除后重新添加。 |

### 考勤机人员关系移除

通过 `POST /dhr/admin/atd/deviceRelation/delete`（别名 `/remove`）移除已分配到考勤机的人员关系。删除后该人员不再与该考勤机关联。

> **阶段边界**：员工离职时自动从考勤机移除人员关系，由离职事件驱动（离职日期为未来日期则在当日凌晨执行，为过去日期则在离职操作时触发）。fixture 阶段不实现离职事件监听。

### 地址展示模式

打卡规则组 `addressShowType` 支持以下展示模式：
- `location`：显示定位地址，打卡记录中展示打卡时 GPS 定位的详细地址。
- `wifi` / `device`：显示设置地址，打卡记录中展示对应考勤地址配置的 `detailAddress` 字段值（可修改为公司名称或园区名称等）。

> **阶段边界**：当员工打卡时 GPS 定位落在两个考勤地址交叉范围内，打卡记录应显示距离更近的考勤地址对应的设置地址。最近地址判定依赖 GPS 距离计算，属于真实打卡计算引擎范畴，fixture 阶段只保存 `addressShowType`、`addressDisplayMode` 等展示配置字段，不做真实距离计算。

### 接口补全汇总

以下接口由 Workers A/B 在本轮补齐，本文档相应章节已更新：

| 新增接口 | 说明 |
|---------|------|
| `POST /dhr/admin/atd/device/update` | 考勤机更新（SN 不可修改） |
| `POST /dhr/admin/atd/deviceRelation/delete` | 移除考勤机人员关系 |
| `POST /dhr/admin/atd/deviceRelation/remove` | 同上，别名路径 |
| `POST /dhr/admin/atd/aiFace/index` | 人脸识别初始化数据 |
| `POST /dhr/admin/atd/aiFace/save` | 新增人脸底库记录 |
| `POST /dhr/admin/atd/aiFace/update` | 更新人脸底库记录 |
| `POST /dhr/admin/atd/aiFace/delete` | 删除人脸底库记录 |

### 阶段边界总览

以下能力不在当前 fixture 阶段范围内，需后续生产逻辑 / 外部集成承接：

| 能力 | 当前状态 | 后续承接方 |
|------|---------|-----------|
| 真实考勤机硬件下发和同步状态 | fixture 只保存下发记录，不调用硬件 SDK | 考勤机硬件集成模块 |
| 云端人脸供应商调用（阿里云/华为云） | fixture 只保存底库记录，不做人脸比对 | 人脸识别集成模块 |
| 真实最近地址（GPS 距离）计算 | fixture 只保存 `addressShowType` 字段 | 打卡计算引擎 |
| 跨时区打卡核算引擎 | fixture 只保存海外打卡字段，不做时区换算 | 打卡计算引擎 |
| 离职事件自动移除考勤机人员 | fixture 不监听离职事件 | 员工生命周期事件模块 |
| 考勤方案引用删除保护（级联校验） | fixture 模拟引用检查，不做级联方案校验 | 考勤方案模块 |
