Skip to content

考勤管理 API

考勤管理模块提供完整的员工考勤打卡、规则管理、统计分析功能。支持上下班打卡、位置验证、迟到早退计算、考勤统计等功能。

功能概述

  • 考勤打卡:支持上班和下班打卡,包含位置验证功能
  • 规则管理:灵活的考勤规则配置,支持工作时间、午休、宽限期等设置
  • 考勤组管理:按组织架构管理考勤组,支持分组设置不同规则
  • 统计分析:提供个人和管理员的考勤统计和数据分析
  • 位置验证:支持GPS定位打卡,限制打卡范围

考勤状态说明

状态值状态文本描述计算规则
0正常正常出勤按时打卡且工作时长达标
1迟到迟到上班打卡超过规定时间(考虑宽限期)
2早退早退下班打卡早于规定时间(考虑宽限期)
3缺勤缺勤无打卡记录或打卡严重异常
4异常异常打卡记录异常或数据不完整

员工考勤接口

上班打卡

员工进行上班打卡,支持位置验证和备注信息。

  • URL: /api/v1/attendance/clock-in
  • 方法: POST
  • 内容类型: application/json
  • 认证: 需要员工权限

请求参数

字段类型必填描述示例
latitudefloat64打卡纬度39.908823
longitudefloat64打卡经度116.397470
notestring备注信息"正常打卡"

请求示例

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
  • 认证: 需要员工权限

请求参数

字段类型必填描述示例
latitudefloat64打卡纬度39.908823
longitudefloat64打卡经度116.397470
notestring备注信息"完成今天工作"

请求示例

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_datestring开始日期"2024-01-01"
end_datestring结束日期"2024-01-31"
statusint状态过滤(0-4)0
pageint页码,默认11
page_sizeint每页数量,默认1020

请求示例

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_datestring开始日期"2024-01-01"
end_datestring结束日期"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_iduint员工ID1
start_datestring开始日期"2024-01-01"
end_datestring结束日期"2024-01-31"
statusint状态过滤(0-4)0
group_iduint考勤组ID1
pageint页码,默认11
page_sizeint每页数量,默认1020

请求示例

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
  • 认证: 需要管理员权限

请求参数

参数类型必填描述示例
iduint考勤记录ID1

响应示例

成功响应 (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
  • 认证: 需要管理员权限

请求参数

字段类型必填描述示例
namestring规则名称"标准工作时间"
descriptionstring规则描述"朝九晚六标准工作时间"
group_iduint考勤组ID1
work_start_timestring上班时间"09:00"
work_end_timestring下班时间"18:00"
break_start_timestring午休开始时间"12:00"
break_end_timestring午休结束时间"13:00"
late_grace_periodint迟到宽限期(分钟)15
early_grace_periodint早退宽限期(分钟)15
required_work_hoursfloat64要求工作时长8.0
allowed_late_daysint允许迟到天数/月3
location_requiredbool是否需要位置验证true
allowed_latitudefloat64允许打卡纬度39.908823
allowed_longitudefloat64允许打卡经度116.397470
location_radiusint允许打卡半径(米)100
work_daysstring工作日"1,2,3,4,5"
flexible_timebool是否弹性工作时间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_iduint考勤组ID1
statusint状态过滤(0-1)1
pageint页码,默认11
page_sizeint每页数量,默认1020
sortstring排序字段"id"
orderstring排序方向(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
  • 认证: 需要管理员权限

请求参数

路径参数

参数类型必填描述示例
iduint考勤规则ID1

请求体参数

字段类型必填描述示例
namestring规则名称"更新后的标准工作时间"
work_start_timestring上班时间"09:30"
statusint状态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
  • 认证: 需要管理员权限

请求参数

参数类型必填描述示例
iduint考勤规则ID1

响应示例

成功响应 (200)

json
{
  "code": 200,
  "message": "删除成功",
  "data": null,
  "timestamp": "2024-01-01T12:00:00Z"
}

考勤组管理

创建考勤组

管理员创建新的考勤组。

  • URL: /api/v1/admin/attendance/groups
  • 方法: POST
  • 内容类型: application/json
  • 认证: 需要管理员权限

请求参数

字段类型必填描述示例
namestring组名"开发部考勤组"
descriptionstring组描述"开发部门考勤管理"
company_iduint公司ID1
manager_iduint负责人ID1

请求示例

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
  • 认证: 需要管理员权限

请求参数

路径参数

参数类型必填描述示例
iduint考勤组ID1

请求体参数

字段类型必填描述示例
employee_idsuint[]员工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
  • 认证: 需要管理员权限

请求参数

参数类型必填描述示例
iduint考勤组ID1

响应示例

成功响应 (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()

相关链接

基于 MIT 许可发布