考勤管理 API
考勤管理模块提供完整的员工考勤打卡、规则管理、统计分析功能。支持上下班打卡、位置验证、迟到早退计算、考勤统计等功能。
功能概述
- 考勤打卡:支持上班和下班打卡,包含位置验证功能
- 规则管理:灵活的考勤规则配置,支持工作时间、午休、宽限期等设置
- 考勤组管理:按组织架构管理考勤组,支持分组设置不同规则
- 统计分析:提供个人和管理员的考勤统计和数据分析
- 位置验证:支持GPS定位打卡,限制打卡范围
考勤状态说明
| 状态值 | 状态文本 | 描述 | 计算规则 |
|---|---|---|---|
| 0 | 正常 | 正常出勤 | 按时打卡且工作时长达标 |
| 1 | 迟到 | 迟到 | 上班打卡超过规定时间(考虑宽限期) |
| 2 | 早退 | 早退 | 下班打卡早于规定时间(考虑宽限期) |
| 3 | 缺勤 | 缺勤 | 无打卡记录或打卡严重异常 |
| 4 | 异常 | 异常 | 打卡记录异常或数据不完整 |
员工考勤接口
上班打卡
员工进行上班打卡,支持位置验证和备注信息。
- URL:
/api/v1/attendance/clock-in - 方法:
POST - 内容类型:
application/json - 认证: 需要员工权限
请求参数
| 字段 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| latitude | float64 | 否 | 打卡纬度 | 39.908823 |
| longitude | float64 | 否 | 打卡经度 | 116.397470 |
| note | string | 否 | 备注信息 | "正常打卡" |
请求示例
json
{
"latitude": 39.908823,
"longitude": 116.397470,
"note": "正常打卡"
}响应示例
成功响应 (200)
json
{
"code": 200,
"message": "打卡成功",
"data": {
"attendance_id": 1,
"clock_in_time": "2024-01-01T09:00:00Z",
"status": 0,
"status_text": "正常",
"late_minutes": 0,
"message": "上班打卡成功"
},
"timestamp": "2024-01-01T12:00:00Z"
}下班打卡
员工进行下班打卡,自动计算工作时长和考勤状态。
- URL:
/api/v1/attendance/clock-out - 方法:
POST - 内容类型:
application/json - 认证: 需要员工权限
请求参数
| 字段 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| latitude | float64 | 否 | 打卡纬度 | 39.908823 |
| longitude | float64 | 否 | 打卡经度 | 116.397470 |
| note | string | 否 | 备注信息 | "完成今天工作" |
请求示例
json
{
"latitude": 39.908823,
"longitude": 116.397470,
"note": "完成今天工作"
}响应示例
成功响应 (200)
json
{
"code": 200,
"message": "打卡成功",
"data": {
"attendance_id": 1,
"clock_out_time": "2024-01-01T18:00:00Z",
"status": 0,
"status_text": "正常",
"work_hours": 8.5,
"early_minutes": 0,
"message": "下班打卡成功,今日工作8.5小时"
},
"timestamp": "2024-01-01T12:00:00Z"
}获取今日考勤
获取当前员工今日的考勤记录。
- URL:
/api/v1/attendance/today - 方法:
GET - 认证: 需要员工权限
响应示例
成功响应 (200)
json
{
"code": 200,
"message": "获取成功",
"data": {
"id": 1,
"employee_id": 1,
"employee": {
"id": 1,
"name": "张三",
"phone": "13800138000"
},
"rule_id": 1,
"rule": {
"id": 1,
"name": "标准工作时间",
"work_start_time": "09:00",
"work_end_time": "18:00"
},
"clock_in_time": "2024-01-01T09:00:00Z",
"clock_out_time": "2024-01-01T18:00:00Z",
"clock_in_lat": 39.908823,
"clock_in_lng": 116.397470,
"clock_out_lat": 39.908823,
"clock_out_lng": 116.397470,
"work_date": "2024-01-01",
"status": 0,
"status_text": "正常",
"work_hours": 8.5,
"late_minutes": 0,
"early_minutes": 0,
"note": "正常打卡",
"created_at": "2024-01-01T09:00:00Z",
"updated_at": "2024-01-01T18:00:00Z"
},
"timestamp": "2024-01-01T12:00:00Z"
}获取个人考勤记录
获取当前员工的考勤记录列表,支持日期范围和状态过滤。
- URL:
/api/v1/attendance/records - 方法:
GET - 认证: 需要员工权限
请求参数
| 参数 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| start_date | string | 否 | 开始日期 | "2024-01-01" |
| end_date | string | 否 | 结束日期 | "2024-01-31" |
| status | int | 否 | 状态过滤(0-4) | 0 |
| page | int | 否 | 页码,默认1 | 1 |
| page_size | int | 否 | 每页数量,默认10 | 20 |
请求示例
GET /api/v1/attendance/records?start_date=2024-01-01&end_date=2024-01-31&status=0&page=1&page_size=20响应示例
成功响应 (200)
json
{
"code": 200,
"message": "获取成功",
"data": {
"records": [
{
"id": 1,
"employee_id": 1,
"employee": {
"id": 1,
"name": "张三",
"phone": "13800138000"
},
"rule_id": 1,
"clock_in_time": "2024-01-01T09:00:00Z",
"clock_out_time": "2024-01-01T18:00:00Z",
"work_date": "2024-01-01",
"status": 0,
"status_text": "正常",
"work_hours": 8.5,
"late_minutes": 0,
"early_minutes": 0,
"note": "正常打卡",
"created_at": "2024-01-01T09:00:00Z",
"updated_at": "2024-01-01T18:00:00Z"
}
],
"pagination": {
"current_page": 1,
"page_size": 20,
"total_items": 1,
"total_pages": 1,
"has_next": false,
"has_prev": false
}
},
"timestamp": "2024-01-01T12:00:00Z"
}获取考勤摘要
获取当前员工的考勤摘要信息,包含今日、本周、本月的考勤情况。
- URL:
/api/v1/attendance/summary - 方法:
GET - 认证: 需要员工权限
响应示例
成功响应 (200)
json
{
"code": 200,
"message": "获取成功",
"data": {
"today": {
"id": 1,
"work_date": "2024-01-01",
"status": 0,
"status_text": "正常",
"clock_in_time": "2024-01-01T09:00:00Z",
"clock_out_time": null,
"work_hours": 0
},
"this_week": {
"total_days": 7,
"work_days": 5,
"attendance_days": 4,
"late_days": 1,
"early_days": 0,
"absent_days": 0,
"exception_days": 0,
"total_work_hours": 34.5,
"avg_work_hours": 8.63,
"total_late_minutes": 15,
"total_early_minutes": 0,
"attendance_rate": 80.0
},
"this_month": {
"total_days": 31,
"work_days": 22,
"attendance_days": 20,
"late_days": 2,
"early_days": 0,
"absent_days": 0,
"exception_days": 0,
"total_work_hours": 176.0,
"avg_work_hours": 8.8,
"total_late_minutes": 30,
"total_early_minutes": 0,
"attendance_rate": 90.9
},
"recent": [
{
"id": 1,
"work_date": "2024-01-01",
"status": 0,
"status_text": "正常",
"work_hours": 8.5
}
]
},
"timestamp": "2024-01-01T12:00:00Z"
}获取考勤统计
获取指定时间段的考勤统计信息。
- URL:
/api/v1/attendance/statistics - 方法:
GET - 认证: 需要员工权限
请求参数
| 参数 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| start_date | string | 是 | 开始日期 | "2024-01-01" |
| end_date | string | 是 | 结束日期 | "2024-01-31" |
请求示例
GET /api/v1/attendance/statistics?start_date=2024-01-01&end_date=2024-01-31响应示例
成功响应 (200)
json
{
"code": 200,
"message": "获取成功",
"data": {
"total_days": 31,
"work_days": 22,
"attendance_days": 20,
"late_days": 2,
"early_days": 0,
"absent_days": 0,
"exception_days": 0,
"total_work_hours": 176.0,
"avg_work_hours": 8.8,
"total_late_minutes": 30,
"total_early_minutes": 0,
"attendance_rate": 90.9
},
"timestamp": "2024-01-01T12:00:00Z"
}获取个人考勤规则
获取当前员工适用的考勤规则。
- URL:
/api/v1/attendance/rule - 方法:
GET - 认证: 需要员工权限
响应示例
成功响应 (200)
json
{
"code": 200,
"message": "获取成功",
"data": {
"id": 1,
"name": "标准工作时间",
"description": "朝九晚六标准工作时间",
"group_id": 1,
"group": {
"id": 1,
"name": "开发部考勤组"
},
"work_start_time": "09:00",
"work_end_time": "18:00",
"break_start_time": "12:00",
"break_end_time": "13:00",
"late_grace_period": 15,
"early_grace_period": 15,
"required_work_hours": 8.0,
"allowed_late_days": 3,
"location_required": true,
"allowed_latitude": 39.908823,
"allowed_longitude": 116.397470,
"location_radius": 100,
"work_days": "1,2,3,4,5",
"flexible_time": false,
"status": 1,
"status_text": "启用",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
},
"timestamp": "2024-01-01T12:00:00Z"
}管理员接口
获取所有考勤记录
管理员获取所有员工的考勤记录,支持多种过滤条件。
- URL:
/api/v1/admin/attendance/records - 方法:
GET - 认证: 需要管理员权限
请求参数
| 参数 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| employee_id | uint | 否 | 员工ID | 1 |
| start_date | string | 否 | 开始日期 | "2024-01-01" |
| end_date | string | 否 | 结束日期 | "2024-01-31" |
| status | int | 否 | 状态过滤(0-4) | 0 |
| group_id | uint | 否 | 考勤组ID | 1 |
| page | int | 否 | 页码,默认1 | 1 |
| page_size | int | 否 | 每页数量,默认10 | 20 |
请求示例
GET /api/v1/admin/attendance/records?start_date=2024-01-01&end_date=2024-01-31&status=1&page=1&page_size=20响应示例
成功响应 (200)
json
{
"code": 200,
"message": "获取成功",
"data": {
"records": [
{
"id": 1,
"employee_id": 1,
"employee": {
"id": 1,
"name": "张三",
"phone": "13800138000"
},
"rule_id": 1,
"clock_in_time": "2024-01-01T09:15:00Z",
"clock_out_time": "2024-01-01T18:00:00Z",
"work_date": "2024-01-01",
"status": 1,
"status_text": "迟到",
"work_hours": 8.25,
"late_minutes": 15,
"early_minutes": 0,
"note": "交通拥堵",
"created_at": "2024-01-01T09:15:00Z",
"updated_at": "2024-01-01T18:00:00Z"
}
],
"pagination": {
"current_page": 1,
"page_size": 20,
"total_items": 1,
"total_pages": 1,
"has_next": false,
"has_prev": false
}
},
"timestamp": "2024-01-01T12:00:00Z"
}获取考勤记录详情
根据ID获取考勤记录详情。
- URL:
/api/v1/admin/attendance/records/{id} - 方法:
GET - 认证: 需要管理员权限
请求参数
| 参数 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| id | uint | 是 | 考勤记录ID | 1 |
响应示例
成功响应 (200)
json
{
"code": 200,
"message": "获取成功",
"data": {
"id": 1,
"employee_id": 1,
"employee": {
"id": 1,
"name": "张三",
"phone": "13800138000"
},
"rule_id": 1,
"rule": {
"id": 1,
"name": "标准工作时间",
"work_start_time": "09:00",
"work_end_time": "18:00"
},
"clock_in_time": "2024-01-01T09:15:00Z",
"clock_out_time": "2024-01-01T18:00:00Z",
"clock_in_lat": 39.908823,
"clock_in_lng": 116.397470,
"clock_out_lat": 39.908823,
"clock_out_lng": 116.397470,
"work_date": "2024-01-01",
"status": 1,
"status_text": "迟到",
"work_hours": 8.25,
"late_minutes": 15,
"early_minutes": 0,
"note": "交通拥堵",
"created_at": "2024-01-01T09:15:00Z",
"updated_at": "2024-01-01T18:00:00Z"
},
"timestamp": "2024-01-01T12:00:00Z"
}考勤规则管理
创建考勤规则
管理员创建新的考勤规则。
- URL:
/api/v1/admin/attendance/rules - 方法:
POST - 内容类型:
application/json - 认证: 需要管理员权限
请求参数
| 字段 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| name | string | 是 | 规则名称 | "标准工作时间" |
| description | string | 否 | 规则描述 | "朝九晚六标准工作时间" |
| group_id | uint | 是 | 考勤组ID | 1 |
| work_start_time | string | 是 | 上班时间 | "09:00" |
| work_end_time | string | 是 | 下班时间 | "18:00" |
| break_start_time | string | 否 | 午休开始时间 | "12:00" |
| break_end_time | string | 否 | 午休结束时间 | "13:00" |
| late_grace_period | int | 否 | 迟到宽限期(分钟) | 15 |
| early_grace_period | int | 否 | 早退宽限期(分钟) | 15 |
| required_work_hours | float64 | 是 | 要求工作时长 | 8.0 |
| allowed_late_days | int | 否 | 允许迟到天数/月 | 3 |
| location_required | bool | 否 | 是否需要位置验证 | true |
| allowed_latitude | float64 | 否 | 允许打卡纬度 | 39.908823 |
| allowed_longitude | float64 | 否 | 允许打卡经度 | 116.397470 |
| location_radius | int | 否 | 允许打卡半径(米) | 100 |
| work_days | string | 是 | 工作日 | "1,2,3,4,5" |
| flexible_time | bool | 否 | 是否弹性工作时间 | false |
请求示例
json
{
"name": "标准工作时间",
"description": "朝九晚六标准工作时间",
"group_id": 1,
"work_start_time": "09:00",
"work_end_time": "18:00",
"break_start_time": "12:00",
"break_end_time": "13:00",
"late_grace_period": 15,
"early_grace_period": 15,
"required_work_hours": 8.0,
"allowed_late_days": 3,
"location_required": true,
"allowed_latitude": 39.908823,
"allowed_longitude": 116.397470,
"location_radius": 100,
"work_days": "1,2,3,4,5",
"flexible_time": false
}响应示例
成功响应 (201)
json
{
"code": 200,
"message": "创建成功",
"data": {
"id": 1,
"name": "标准工作时间",
"description": "朝九晚六标准工作时间",
"group_id": 1,
"work_start_time": "09:00",
"work_end_time": "18:00",
"break_start_time": "12:00",
"break_end_time": "13:00",
"late_grace_period": 15,
"early_grace_period": 15,
"required_work_hours": 8.0,
"allowed_late_days": 3,
"location_required": true,
"allowed_latitude": 39.908823,
"allowed_longitude": 116.397470,
"location_radius": 100,
"work_days": "1,2,3,4,5",
"flexible_time": false,
"status": 1,
"status_text": "启用",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
},
"timestamp": "2024-01-01T12:00:00Z"
}获取考勤规则列表
获取考勤规则列表。
- URL:
/api/v1/admin/attendance/rules - 方法:
GET - 认证: 需要管理员权限
请求参数
| 参数 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| group_id | uint | 否 | 考勤组ID | 1 |
| status | int | 否 | 状态过滤(0-1) | 1 |
| page | int | 否 | 页码,默认1 | 1 |
| page_size | int | 否 | 每页数量,默认10 | 20 |
| sort | string | 否 | 排序字段 | "id" |
| order | string | 否 | 排序方向(asc/desc) | "desc" |
请求示例
GET /api/v1/admin/attendance/rules?group_id=1&status=1&page=1&page_size=20响应示例
成功响应 (200)
json
{
"code": 200,
"message": "获取成功",
"data": {
"rules": [
{
"id": 1,
"name": "标准工作时间",
"description": "朝九晚六标准工作时间",
"group_id": 1,
"group": {
"id": 1,
"name": "开发部考勤组"
},
"work_start_time": "09:00",
"work_end_time": "18:00",
"break_start_time": "12:00",
"break_end_time": "13:00",
"late_grace_period": 15,
"early_grace_period": 15,
"required_work_hours": 8.0,
"allowed_late_days": 3,
"location_required": true,
"allowed_latitude": 39.908823,
"allowed_longitude": 116.397470,
"location_radius": 100,
"work_days": "1,2,3,4,5",
"flexible_time": false,
"status": 1,
"status_text": "启用",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
],
"pagination": {
"current_page": 1,
"page_size": 20,
"total_items": 1,
"total_pages": 1,
"has_next": false,
"has_prev": false
}
},
"timestamp": "2024-01-01T12:00:00Z"
}更新考勤规则
更新指定的考勤规则。
- URL:
/api/v1/admin/attendance/rules/{id} - 方法:
PUT - 内容类型:
application/json - 认证: 需要管理员权限
请求参数
路径参数
| 参数 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| id | uint | 是 | 考勤规则ID | 1 |
请求体参数
| 字段 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| name | string | 否 | 规则名称 | "更新后的标准工作时间" |
| work_start_time | string | 否 | 上班时间 | "09:30" |
| status | int | 否 | 状态 | 1 |
请求示例
json
{
"name": "更新后的标准工作时间",
"work_start_time": "09:30",
"status": 1
}响应示例
成功响应 (200)
json
{
"code": 200,
"message": "更新成功",
"data": {
"id": 1,
"name": "更新后的标准工作时间",
"work_start_time": "09:30",
"work_end_time": "18:00",
"status": 1,
"status_text": "启用",
"updated_at": "2024-01-01T12:00:00Z"
},
"timestamp": "2024-01-01T12:00:00Z"
}删除考勤规则
删除指定的考勤规则。
- URL:
/api/v1/admin/attendance/rules/{id} - 方法:
DELETE - 认证: 需要管理员权限
请求参数
| 参数 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| id | uint | 是 | 考勤规则ID | 1 |
响应示例
成功响应 (200)
json
{
"code": 200,
"message": "删除成功",
"data": null,
"timestamp": "2024-01-01T12:00:00Z"
}考勤组管理
创建考勤组
管理员创建新的考勤组。
- URL:
/api/v1/admin/attendance/groups - 方法:
POST - 内容类型:
application/json - 认证: 需要管理员权限
请求参数
| 字段 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| name | string | 是 | 组名 | "开发部考勤组" |
| description | string | 否 | 组描述 | "开发部门考勤管理" |
| company_id | uint | 是 | 公司ID | 1 |
| manager_id | uint | 否 | 负责人ID | 1 |
请求示例
json
{
"name": "开发部考勤组",
"description": "开发部门考勤管理",
"company_id": 1,
"manager_id": 1
}响应示例
成功响应 (201)
json
{
"code": 200,
"message": "创建成功",
"data": {
"id": 1,
"name": "开发部考勤组",
"description": "开发部门考勤管理",
"company_id": 1,
"company": {
"id": 1,
"name": "源丰科技有限公司"
},
"manager_id": 1,
"manager": {
"id": 1,
"name": "张三",
"phone": "13800138000"
},
"status": 1,
"status_text": "启用",
"employee_count": 0,
"rule_count": 0,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
},
"timestamp": "2024-01-01T12:00:00Z"
}添加员工到考勤组
管理员添加员工到指定考勤组。
- URL:
/api/v1/admin/attendance/groups/{id}/employees - 方法:
POST - 内容类型:
application/json - 认证: 需要管理员权限
请求参数
路径参数
| 参数 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| id | uint | 是 | 考勤组ID | 1 |
请求体参数
| 字段 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| employee_ids | uint[] | 是 | 员工ID列表 | [1, 2, 3] |
请求示例
json
{
"employee_ids": [1, 2, 3]
}响应示例
成功响应 (200)
json
{
"code": 200,
"message": "添加成功",
"data": null,
"timestamp": "2024-01-01T12:00:00Z"
}获取考勤组员工列表
获取指定考勤组的员工列表。
- URL:
/api/v1/admin/attendance/groups/{id}/employees - 方法:
GET - 认证: 需要管理员权限
请求参数
| 参数 | 类型 | 必填 | 描述 | 示例 |
|---|---|---|---|---|
| id | uint | 是 | 考勤组ID | 1 |
响应示例
成功响应 (200)
json
{
"code": 200,
"message": "获取成功",
"data": [
{
"id": 1,
"name": "张三",
"phone": "13800138000"
},
{
"id": 2,
"name": "李四",
"phone": "13800138001"
}
],
"timestamp": "2024-01-01T12:00:00Z"
}数据模型
AttendanceResponse
typescript
interface AttendanceResponse {
id: number; // 记录ID
employee_id: number; // 员工ID
employee?: EmployeeSimpleResponse; // 员工信息
rule_id: number; // 规则ID
rule?: AttendanceRuleResponse; // 规则信息
clock_in_time?: string; // 上班打卡时间
clock_out_time?: string; // 下班打卡时间
clock_in_lat?: number; // 上班打卡纬度
clock_in_lng?: number; // 上班打卡经度
clock_out_lat?: number; // 下班打卡纬度
clock_out_lng?: number; // 下班打卡经度
work_date: string; // 工作日期
status: number; // 状态
status_text: string; // 状态文本
work_hours: number; // 工作时长
late_minutes: number; // 迟到分钟数
early_minutes: number; // 早退分钟数
note: string; // 备注
created_at: string; // 创建时间
updated_at: string; // 更新时间
}AttendanceRuleResponse
typescript
interface AttendanceRuleResponse {
id: number; // 规则ID
name: string; // 规则名称
description: string; // 规则描述
group_id: number; // 考勤组ID
group?: AttendanceGroupResponse; // 考勤组信息
work_start_time: string; // 上班时间
work_end_time: string; // 下班时间
break_start_time?: string; // 午休开始时间
break_end_time?: string; // 午休结束时间
late_grace_period: number; // 迟到宽限期
early_grace_period: number; // 早退宽限期
required_work_hours: number; // 要求工作时长
allowed_late_days: number; // 允许迟到天数
location_required: boolean; // 是否需要位置验证
allowed_latitude?: number; // 允许打卡纬度
allowed_longitude?: number; // 允许打卡经度
location_radius: number; // 允许打卡半径
work_days: string; // 工作日
flexible_time: boolean; // 是否弹性工作时间
status: number; // 状态
status_text: string; // 状态文本
created_at: string; // 创建时间
updated_at: string; // 更新时间
}AttendanceGroupResponse
typescript
interface AttendanceGroupResponse {
id: number; // 组ID
name: string; // 组名
description: string; // 组描述
company_id: number; // 公司ID
company?: CompanySimpleResponse; // 公司信息
manager_id: number; // 负责人ID
manager?: EmployeeSimpleResponse; // 负责人信息
status: number; // 状态
status_text: string; // 状态文本
employee_count: number; // 员工数量
rule_count: number; // 规则数量
created_at: string; // 创建时间
updated_at: string; // 更新时间
}AttendanceStatisticsResponse
typescript
interface AttendanceStatisticsResponse {
total_days: number; // 总天数
work_days: number; // 工作日天数
attendance_days: number; // 出勤天数
late_days: number; // 迟到天数
early_days: number; // 早退天数
absent_days: number; // 缺勤天数
exception_days: number; // 异常天数
total_work_hours: number; // 总工作时长
avg_work_hours: number; // 平均工作时长
total_late_minutes: number; // 总迟到分钟
total_early_minutes: number; // 总早退分钟
attendance_rate: number; // 出勤率
}工作日说明
工作日使用逗号分隔的数字表示,对应周一到周日:
| 数字 | 星期 | 说明 |
|---|---|---|
| 1 | 周一 | |
| 2 | 周二 | |
| 3 | 周三 | |
| 4 | 周四 | |
| 5 | 周五 | |
| 6 | 周六 | |
| 7 | 周日 |
例如:"1,2,3,4,5" 表示周一到周五为工作日。
位置验证
当考勤规则启用了位置验证时:
- 系统会验证打卡位置与允许的坐标距离
- 只允许在指定半径范围内打卡
- 支持GPS定位验证打卡位置
弹性工作时间
当启用弹性工作时间时:
- 员工可以在指定时间范围内弹性打卡
- 主要计算工作时长是否达标
- 迟到早退判定相对宽松
错误代码
| 错误代码 | 描述 | 解决方案 |
|---|---|---|
| 400 | 请求参数错误 | 检查请求参数格式和必填字段 |
| 401 | 未认证 | 检查 JWT token 是否有效 |
| 403 | 无权限 | 确认用户具有相应权限 |
| 404 | 记录不存在 | 检查记录ID是否正确 |
| 409 | 重复打卡 | 今日已打卡,请勿重复操作 |
| 500 | 服务器内部错误 | 联系系统管理员 |
集成示例
React 考勤打卡组件
javascript
import React, { useState, useEffect } from 'react';
const AttendanceClock = () => {
const [todayAttendance, setTodayAttendance] = useState(null);
const [loading, setLoading] = useState(false);
const [location, setLocation] = useState(null);
const [note, setNote] = useState('');
// 获取当前位置
const getCurrentLocation = () => {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) {
reject(new Error('浏览器不支持地理位置'));
return;
}
navigator.geolocation.getCurrentPosition(
(position) => {
resolve({
latitude: position.coords.latitude,
longitude: position.coords.longitude
});
},
(error) => {
reject(error);
},
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 60000
}
);
});
};
// 获取今日考勤
const fetchTodayAttendance = async () => {
try {
const response = await fetch('/api/v1/attendance/today', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
const data = await response.json();
if (data.code === 200) {
setTodayAttendance(data.data);
}
} catch (error) {
console.error('获取今日考勤失败:', error);
}
};
// 上班打卡
const handleClockIn = async () => {
setLoading(true);
try {
let requestData = { note };
// 获取位置信息
try {
const loc = await getCurrentLocation();
requestData.latitude = loc.latitude;
requestData.longitude = loc.longitude;
} catch (error) {
console.warn('获取位置失败:', error);
}
const response = await fetch('/api/v1/attendance/clock-in', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify(requestData)
});
const data = await response.json();
if (data.code === 200) {
alert(data.message);
fetchTodayAttendance();
setNote('');
} else {
alert(data.message || '打卡失败');
}
} catch (error) {
console.error('上班打卡失败:', error);
alert('打卡失败,请重试');
} finally {
setLoading(false);
}
};
// 下班打卡
const handleClockOut = async () => {
setLoading(true);
try {
let requestData = { note };
// 获取位置信息
try {
const loc = await getCurrentLocation();
requestData.latitude = loc.latitude;
requestData.longitude = loc.longitude;
} catch (error) {
console.warn('获取位置失败:', error);
}
const response = await fetch('/api/v1/attendance/clock-out', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify(requestData)
});
const data = await response.json();
if (data.code === 200) {
alert(data.message);
fetchTodayAttendance();
setNote('');
} else {
alert(data.message || '打卡失败');
}
} catch (error) {
console.error('下班打卡失败:', error);
alert('打卡失败,请重试');
} finally {
setLoading(false);
}
};
const getStatusColor = (status) => {
const colors = {
0: '#28a745', // 正常 - 绿色
1: '#ffc107', // 迟到 - 黄色
2: '#fd7e14', // 早退 - 橙色
3: '#dc3545', // 缺勤 - 红色
4: '#6f42c1' // 异常 - 紫色
};
return colors[status] || '#6c757d';
};
const formatTime = (timeString) => {
if (!timeString) return '--';
return new Date(timeString).toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
});
};
useEffect(() => {
fetchTodayAttendance();
}, []);
const canClockIn = !todayAttendance || !todayAttendance.clock_in_time;
const canClockOut = todayAttendance && todayAttendance.clock_in_time && !todayAttendance.clock_out_time;
return (
<div className="attendance-clock">
<h2>考勤打卡</h2>
{todayAttendance && (
<div className="today-status">
<h3>今日考勤状态</h3>
<div className="status-info">
<div className="status-badge" style={{ backgroundColor: getStatusColor(todayAttendance.status) }}>
{todayAttendance.status_text}
</div>
<div className="time-info">
<div className="time-item">
<span className="label">上班时间:</span>
<span className="time">{formatTime(todayAttendance.clock_in_time)}</span>
</div>
<div className="time-item">
<span className="label">下班时间:</span>
<span className="time">{formatTime(todayAttendance.clock_out_time)}</span>
</div>
<div className="time-item">
<span className="label">工作时长:</span>
<span className="time">{todayAttendance.work_hours || 0} 小时</span>
</div>
</div>
</div>
</div>
)}
<div className="clock-section">
<div className="input-group">
<label>备注:</label>
<input
type="text"
value={note}
onChange={(e) => setNote(e.target.value)}
placeholder="请输入备注信息"
maxLength={500}
/>
</div>
<div className="button-group">
{canClockIn && (
<button
className="clock-btn clock-in-btn"
onClick={handleClockIn}
disabled={loading}
>
{loading ? '打卡中...' : '上班打卡'}
</button>
)}
{canClockOut && (
<button
className="clock-btn clock-out-btn"
onClick={handleClockOut}
disabled={loading}
>
{loading ? '打卡中...' : '下班打卡'}
</button>
)}
{!canClockIn && !canClockOut && (
<div className="completed-message">
今日考勤已完成
</div>
)}
</div>
</div>
<style jsx>{`
.attendance-clock {
max-width: 500px;
margin: 0 auto;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.attendance-clock h2 {
text-align: center;
margin-bottom: 20px;
color: #333;
}
.today-status {
margin-bottom: 30px;
padding: 15px;
background: #f8f9fa;
border-radius: 6px;
}
.today-status h3 {
margin: 0 0 15px 0;
color: #495057;
}
.status-info {
display: flex;
align-items: center;
gap: 15px;
}
.status-badge {
padding: 6px 12px;
border-radius: 20px;
color: white;
font-weight: bold;
font-size: 14px;
}
.time-info {
flex: 1;
}
.time-item {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
font-size: 14px;
}
.time-item .label {
color: #6c757d;
}
.time-item .time {
font-weight: bold;
color: #333;
}
.clock-section {
margin-top: 20px;
}
.input-group {
margin-bottom: 20px;
}
.input-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #333;
}
.input-group input {
width: 100%;
padding: 10px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 14px;
}
.button-group {
display: flex;
gap: 10px;
justify-content: center;
}
.clock-btn {
padding: 12px 30px;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.2s;
}
.clock-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.clock-in-btn {
background: #28a745;
color: white;
}
.clock-in-btn:hover:not(:disabled) {
background: #218838;
}
.clock-out-btn {
background: #007bff;
color: white;
}
.clock-out-btn:hover:not(:disabled) {
background: #0056b3;
}
.completed-message {
padding: 12px 30px;
background: #e9ecef;
color: #6c757d;
border-radius: 6px;
font-weight: bold;
text-align: center;
}
`}</style>
</div>
);
};
export default AttendanceClock;Python 考勤管理脚本
python
import requests
import json
from datetime import datetime, timedelta
from typing import List, Optional, Dict, Any
class AttendanceManager:
def __init__(self, base_url: str, admin_token: str, employee_token: str):
self.base_url = base_url
self.admin_token = admin_token
self.employee_token = employee_token
self.admin_headers = {
'Authorization': f'Bearer {admin_token}',
'Content-Type': 'application/json'
}
self.employee_headers = {
'Authorization': f'Bearer {employee_token}',
'Content-Type': 'application/json'
}
def clock_in(self, latitude: Optional[float] = None,
longitude: Optional[float] = None,
note: str = "") -> Dict[str, Any]:
"""上班打卡"""
data = {'note': note}
if latitude is not None:
data['latitude'] = latitude
if longitude is not None:
data['longitude'] = longitude
response = requests.post(
f'{self.base_url}/api/v1/attendance/clock-in',
json=data,
headers=self.employee_headers
)
response.raise_for_status()
return response.json()
def clock_out(self, latitude: Optional[float] = None,
longitude: Optional[float] = None,
note: str = "") -> Dict[str, Any]:
"""下班打卡"""
data = {'note': note}
if latitude is not None:
data['latitude'] = latitude
if longitude is not None:
data['longitude'] = longitude
response = requests.post(
f'{self.base_url}/api/v1/attendance/clock-out',
json=data,
headers=self.employee_headers
)
response.raise_for_status()
return response.json()
def get_today_attendance(self) -> Dict[str, Any]:
"""获取今日考勤"""
response = requests.get(
f'{self.base_url}/api/v1/attendance/today',
headers=self.employee_headers
)
response.raise_for_status()
return response.json()
def get_attendance_records(self, start_date: Optional[str] = None,
end_date: Optional[str] = None,
status: Optional[int] = None,
page: int = 1, page_size: int = 20) -> Dict[str, Any]:
"""获取考勤记录"""
params = {
'page': page,
'page_size': page_size
}
if start_date:
params['start_date'] = start_date
if end_date:
params['end_date'] = end_date
if status is not None:
params['status'] = status
response = requests.get(
f'{self.base_url}/api/v1/attendance/records',
params=params,
headers=self.employee_headers
)
response.raise_for_status()
return response.json()
def get_attendance_statistics(self, start_date: str,
end_date: str) -> Dict[str, Any]:
"""获取考勤统计"""
params = {
'start_date': start_date,
'end_date': end_date
}
response = requests.get(
f'{self.base_url}/api/v1/attendance/statistics',
params=params,
headers=self.employee_headers
)
response.raise_for_status()
return response.json()
def get_attendance_summary(self) -> Dict[str, Any]:
"""获取考勤摘要"""
response = requests.get(
f'{self.base_url}/api/v1/attendance/summary',
headers=self.employee_headers
)
response.raise_for_status()
return response.json()
def get_my_attendance_rule(self) -> Dict[str, Any]:
"""获取个人考勤规则"""
response = requests.get(
f'{self.base_url}/api/v1/attendance/rule',
headers=self.employee_headers
)
response.raise_for_status()
return response.json()
# 管理员方法
def get_all_attendance_records(self, employee_id: Optional[int] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
status: Optional[int] = None,
group_id: Optional[int] = None,
page: int = 1, page_size: int = 20) -> Dict[str, Any]:
"""获取所有考勤记录(管理员)"""
params = {
'page': page,
'page_size': page_size
}
if employee_id:
params['employee_id'] = employee_id
if start_date:
params['start_date'] = start_date
if end_date:
params['end_date'] = end_date
if status is not None:
params['status'] = status
if group_id:
params['group_id'] = group_id
response = requests.get(
f'{self.base_url}/api/v1/admin/attendance/records',
params=params,
headers=self.admin_headers
)
response.raise_for_status()
return response.json()
def create_attendance_rule(self, name: str, group_id: int,
work_start_time: str, work_end_time: str,
required_work_hours: float,
**kwargs) -> Dict[str, Any]:
"""创建考勤规则"""
data = {
'name': name,
'group_id': group_id,
'work_start_time': work_start_time,
'work_end_time': work_end_time,
'required_work_hours': required_work_hours
}
data.update(kwargs)
response = requests.post(
f'{self.base_url}/api/v1/admin/attendance/rules',
json=data,
headers=self.admin_headers
)
response.raise_for_status()
return response.json()
def create_attendance_group(self, name: str, company_id: int,
description: str = "",
manager_id: Optional[int] = None) -> Dict[str, Any]:
"""创建考勤组"""
data = {
'name': name,
'company_id': company_id,
'description': description
}
if manager_id:
data['manager_id'] = manager_id
response = requests.post(
f'{self.base_url}/api/v1/admin/attendance/groups',
json=data,
headers=self.admin_headers
)
response.raise_for_status()
return response.json()
def add_employees_to_group(self, group_id: int,
employee_ids: List[int]) -> Dict[str, Any]:
"""添加员工到考勤组"""
response = requests.post(
f'{self.base_url}/api/v1/admin/attendance/groups/{group_id}/employees',
json={'employee_ids': employee_ids},
headers=self.admin_headers
)
response.raise_for_status()
return response.json()
def export_attendance_to_csv(self, filename: str, start_date: str,
end_date: str) -> None:
"""导出考勤记录到CSV文件"""
all_records = []
page = 1
while True:
data = self.get_all_attendance_records(
start_date=start_date,
end_date=end_date,
page=page,
page_size=100
)
records = data['data']['records']
if not records:
break
all_records.extend(records)
page += 1
# 写入CSV文件
import csv
with open(filename, 'w', newline='', encoding='utf-8') as csvfile:
if not all_records:
print("没有找到考勤记录")
return
fieldnames = [
'id', 'employee_name', 'work_date', 'clock_in_time',
'clock_out_time', 'status_text', 'work_hours',
'late_minutes', 'early_minutes', 'note'
]
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
for record in all_records:
writer.writerow({
'id': record['id'],
'employee_name': record.get('employee', {}).get('name', ''),
'work_date': record['work_date'],
'clock_in_time': record.get('clock_in_time', ''),
'clock_out_time': record.get('clock_out_time', ''),
'status_text': record['status_text'],
'work_hours': record['work_hours'],
'late_minutes': record['late_minutes'],
'early_minutes': record['early_minutes'],
'note': record['note']
})
print(f"已导出 {len(all_records)} 条考勤记录到 {filename}")
def generate_attendance_report(self, start_date: str,
end_date: str) -> str:
"""生成考勤报告"""
# 获取统计数据
stats_data = self.get_attendance_statistics(start_date, end_date)
stats = stats_data['data']
# 获取详细记录
records_data = self.get_all_attendance_records(
start_date=start_date,
end_date=end_date,
page_size=1000
)
report = f"""
# 考勤统计报告
## 统计周期
- 开始日期: {start_date}
- 结束日期: {end_date}
## 总体统计
- 总天数: {stats['total_days']} 天
- 工作日: {stats['work_days']} 天
- 出勤天数: {stats['attendance_days']} 天
- 出勤率: {stats['attendance_rate']:.1f}%
## 考勤详情
- 正常出勤: {stats['attendance_days'] - stats['late_days'] - stats['early_days']} 天
- 迟到: {stats['late_days']} 天
- 早退: {stats['early_days']} 天
- 缺勤: {stats['absent_days']} 天
- 异常: {stats['exception_days']} 天
## 工作时长统计
- 总工作时长: {stats['total_work_hours']:.1f} 小时
- 平均工作时长: {stats['avg_work_hours']:.1f} 小时
- 总迟到时间: {stats['total_late_minutes']} 分钟
- 总早退时间: {stats['total_early_minutes']} 分钟
## 详细记录
| 日期 | 员工 | 上班时间 | 下班时间 | 工作时长 | 状态 | 备注 |
|------|------|----------|----------|----------|------|------|
"""
for record in records_data['data']['records']:
employee_name = record.get('employee', {}).get('name', '未知')
clock_in = record.get('clock_in_time', '--')[:5] if record.get('clock_in_time') else '--'
clock_out = record.get('clock_out_time', '--')[:5] if record.get('clock_out_time') else '--'
report += f"| {record['work_date']} | {employee_name} | {clock_in} | {clock_out} | "
report += f"{record['work_hours']:.1f}h | {record['status_text']} | {record['note']} |\n"
return report
# 使用示例
def main():
# 初始化考勤管理器
attendance_manager = AttendanceManager(
base_url='http://localhost:8080',
admin_token='your_admin_token',
employee_token='your_employee_token'
)
print("=== 考勤管理示例 ===")
# 员工操作
print("\n--- 员工操作 ---")
# 获取今日考勤
try:
today_data = attendance_manager.get_today_attendance()
if today_data['data']:
today = today_data['data']
print(f"今日状态: {today['status_text']}")
print(f"上班时间: {today.get('clock_in_time', '未打卡')}")
print(f"下班时间: {today.get('clock_out_time', '未打卡')}")
else:
print("今日尚未打卡")
except Exception as e:
print(f"获取今日考勤失败: {e}")
# 上班打卡
try:
result = attendance_manager.clock_in(
latitude=39.908823,
longitude=116.397470,
note="正常打卡"
)
print(f"上班打卡成功: {result['message']}")
except Exception as e:
print(f"上班打卡失败: {e}")
# 考勤统计
try:
# 本月统计
today = datetime.now()
first_day = today.replace(day=1).strftime('%Y-%m-%d')
today_str = today.strftime('%Y-%m-%d')
stats = attendance_manager.get_attendance_statistics(first_day, today_str)
print(f"本月考勤统计:")
print(f" 出勤天数: {stats['data']['attendance_days']}")
print(f" 迟到天数: {stats['data']['late_days']}")
print(f" 出勤率: {stats['data']['attendance_rate']:.1f}%")
except Exception as e:
print(f"获取考勤统计失败: {e}")
# 管理员操作
print("\n--- 管理员操作 ---")
# 创建考勤组
try:
result = attendance_manager.create_attendance_group(
name="测试考勤组",
company_id=1,
description="测试用的考勤组"
)
print(f"创建考勤组成功: {result['message']}")
group_id = result['data']['id']
except Exception as e:
print(f"创建考勤组失败: {e}")
group_id = 1
# 创建考勤规则
try:
result = attendance_manager.create_attendance_rule(
name="弹性工作时间",
group_id=group_id,
work_start_time="09:00",
work_end_time="18:00",
required_work_hours=8.0,
description="9点到6点,午休1小时",
break_start_time="12:00",
break_end_time="13:00",
flexible_time=True,
location_required=True,
location_radius=200
)
print(f"创建考勤规则成功: {result['message']}")
except Exception as e:
print(f"创建考勤规则失败: {e}")
# 导出考勤记录
try:
attendance_manager.export_attendance_to_csv(
'attendance_report.csv',
'2024-01-01',
'2024-01-31'
)
print("考勤记录导出完成")
except Exception as e:
print(f"导出考勤记录失败: {e}")
# 生成考勤报告
try:
report = attendance_manager.generate_attendance_report(
'2024-01-01',
'2024-01-31'
)
print(report)
except Exception as e:
print(f"生成考勤报告失败: {e}")
if __name__ == '__main__':
main()