1. 项目概述:一个飞书考勤数据的自动化处理工具
最近在团队内部做了一次小范围的自动化工具分享,聊到了一个我自己维护了挺久的小项目:feishu-inout。这本质上是一个专门用来处理飞书(Lark)考勤数据的命令行工具。如果你或者你的团队正在使用飞书作为办公协同平台,并且每天都需要手动导出、整理、核对考勤打卡记录,那么这个工具可能会让你从繁琐的重复劳动中解放出来。
简单来说,feishu-inout的核心功能就是帮你自动从飞书后台拉取指定时间范围内的员工打卡原始数据,然后进行清洗、规整,最终输出成一份结构清晰、易于分析的表格(通常是 CSV 格式)。它解决的核心痛点是:飞书管理后台导出的原始考勤数据往往夹杂着大量冗余信息,格式也不够友好,直接用于统计分析或薪酬计算非常低效。手动处理不仅耗时,还容易出错。这个工具就是把“登录后台 -> 选择日期 -> 导出报表 -> 用Excel清洗”这一套流程,压缩成一条简单的命令。
它适合谁呢?首先是团队的管理者或HR,需要定期核对考勤情况;其次是负责薪酬计算的同事;再者,对于有开发背景、希望将考勤数据与其他内部系统(如OA、ERP)打通的工程师来说,它提供了一个可靠的数据获取接口。即使你不是开发者,只要能在终端里运行命令,也能轻松上手。接下来,我会详细拆解这个工具的设计思路、核心实现以及我在实际使用中积累的一些经验。
2. 核心需求与设计思路拆解
2.1 原始考勤数据的痛点分析
在动手写任何代码之前,搞清楚我们到底要解决什么问题至关重要。飞书作为一款优秀的产品,其考勤模块功能已经相当完善。但是,当我们试图将考勤数据用于二次处理时,就会遇到几个典型的“最后一公里”问题。
首先,数据导出流程繁琐。你需要以管理员身份登录飞书后台,找到考勤统计模块,然后精确选择日期范围和人员范围,才能点击导出。这个过程无法自动化,每次都需要人工干预。
其次,原始数据格式“脏乱”。导出的 CSV 或 Excel 文件,往往包含了大量对于数据分析无用的字段,比如每次操作的记录ID、一些内部状态码等。同时,有用的信息可能分散在不同的列,或者以不易处理的格式存在(例如,打卡时间可能和日期混在一个单元格里)。
第三,缺乏灵活的筛选与聚合。后台导出的通常是全量明细数据。如果你只想看某个部门的迟到情况,或者统计每个人本月的外出时长,你需要在 Excel 里进行复杂的筛选、数据透视表操作。对于周期性报告,这些操作每次都要重复。
最后,难以集成自动化流程。手动导出的数据文件是一个孤岛,很难无缝对接到后续的自动化处理流程中,比如自动计算薪资、同步到 BI 报表系统等。
feishu-inout的设计目标,就是通过程序化调用飞书开放平台的 API,绕过人工操作,直接获取原始数据,并在一开始就将其处理成干净、结构化的格式,为后续的所有应用场景打下基础。
2.2 技术方案选型:为什么是命令行工具 + Go语言?
面对上述需求,有多种技术实现路径可选:比如写一个浏览器插件来自动点击导出按钮;或者构建一个带有前端页面的 Web 应用;又或者开发一个桌面软件。我最终选择了开发一个命令行工具,并用 Go 语言来实现,主要基于以下几点考量:
1. 命令行工具的优势:
- 极致的自动化友好性:命令行工具可以轻松地被脚本(如 Shell、Python)、定时任务(如 Crontab、Jenkins)或 CI/CD 流程调用,这是实现全自动化的基石。
- 低开销与易部署:它通常是一个独立的可执行文件,无需安装运行时环境(特别是 Go 编译的二进制文件),在服务器、个人电脑甚至容器里都能即开即用。
- 清晰的输入输出:通过命令行参数指定查询条件(如起止日期),执行结果直接输出到标准输出或文件,逻辑清晰,易于和其他工具(如
grep,awk,jq)组合使用。
2. 选择 Go 语言的考量:
- 强大的标准库与并发能力:Go 的标准库对 HTTP 请求、JSON 处理、CSV 读写等支持得非常好,几乎不需要依赖第三方库就能完成核心功能。同时,如果需要并发获取大量员工的数据,Go 的 Goroutine 模型能非常优雅且高效地实现。
- 编译为单一二进制文件:这正是部署简便性的关键。用户只需要下载这个文件,赋予执行权限即可运行,没有任何复杂的依赖问题,跨平台编译也相对容易。
- 良好的可维护性:代码结构清晰,静态类型系统能在编译期发现很多错误,对于这种需要长期维护、可能被多人使用的小工具来说,非常合适。
注意:这个工具并非飞书官方的 SDK,它是我基于飞书开放平台的 API 文档,封装了考勤数据获取这一特定场景的便捷工具。因此,它的功能和稳定性与飞书 API 本身紧密相关。
3. 核心实现细节与飞书 API 对接
3.1 飞书开放平台接入准备
要让工具能访问考勤数据,第一步必须在飞书开放平台上创建一个应用,并获取必要的凭证。这个过程虽然有些步骤,但一劳永逸。
1. 创建企业自建应用:登录飞书开放平台,进入开发者后台。你需要创建一个“企业自建应用”。给应用起个名字,比如“考勤数据助手”。创建成功后,你会得到两个核心凭证:
App ID: 应用的唯一标识。App Secret: 相当于应用密码,用于获取访问令牌,必须严格保密。
2. 配置应用权限:这是最关键的一步。应用必须被授予相应的权限才能访问考勤数据。在应用的“权限管理”页面,你需要找到并添加以下权限:
contact:user.base:readonly(获取用户信息)contact:user.employment:readonly(获取用户雇佣信息,用于部门筛选)attendance:attendance:readonly(核心权限:读取考勤数据) 添加后,记得在页面底部“权限管理”中,将权限版本发布。
3. 获取访问令牌 (Access Token):飞书 API 几乎所有的调用都需要在请求头中携带Authorization: Bearer {access_token}。这个access_token需要通过App ID和App Secret来换取。工具内部需要实现一个自动获取和刷新 Token 的机制。通常,Token 有效期为2小时,所以工具要么在每次运行时获取一个新的,要么实现一个简单的缓存机制,在 Token 过期前复用。
// 这是一个简化的 Go 代码示例,展示获取 Tenant Access Token 的逻辑 package main import ( "encoding/json" "fmt" "io/ioutil" "net/http" ) type TokenResponse struct { Code int `json:"code"` Msg string `json:"msg"` TenantAccessToken string `json:"tenant_access_token"` Expire int `json:"expire"` } func getFeishuToken(appID, appSecret string) (string, error) { url := "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal" reqBody := fmt.Sprintf(`{"app_id": "%s", "app_secret": "%s"}`, appID, appSecret) resp, err := http.Post(url, "application/json", strings.NewReader(reqBody)) if err != nil { return "", err } defer resp.Body.Close() body, _ := ioutil.ReadAll(resp.Body) var tokenResp TokenResponse json.Unmarshal(body, &tokenResp) if tokenResp.Code != 0 { return "", fmt.Errorf("failed to get token: %s", tokenResp.Msg) } return tokenResp.TenantAccessToken, nil }3.2 考勤记录获取 API 的调用与分页处理
飞书提供了GET /attendance/v1/user_task_reports/query接口来查询用户打卡结果。这个接口功能强大,但调用时有一些细节需要特别注意。
核心请求参数:
employee_type: 固定为employee_id,表示我们使用飞书内部的人员ID。ignore_invalid_users: 建议设为true,避免因个别无效用户导致整个查询失败。include_terminated_users: 根据是否需要包含离职人员考勤来设定。user_ids: 要查询的用户ID列表。这是关键,如果不传,则默认查询全公司人员,可能触发频率限制或返回超时。最佳实践是结合部门树,分批查询。check_date_from&check_date_to: 查询的日期范围,格式为yyyy-MM-dd。
最大的挑战:分页与数据量该接口返回的数据是分页的。即使你只查一天,如果公司人数多,打卡记录也多,单次响应可能无法包含全部数据。响应体中的has_more字段和page_token字段就是用于处理分页的。
工具内的分页处理逻辑:
- 首次调用,不传
page_token。 - 解析响应,保存当前页的
user_task_results(打卡结果)。 - 检查
has_more是否为true。如果是,将响应中的page_token取出,作为下一次请求的参数。 - 重复步骤2-3,直到
has_more为false。 - 将所有批次的结果合并。
// 简化的分页查询逻辑 func queryAttendanceRecords(token string, fromDate, toDate string, userIDs []string) ([]AttendanceRecord, error) { var allRecords []AttendanceRecord var pageToken string for { reqBody := map[string]interface{}{ "employee_type": "employee_id", "ignore_invalid_users": true, "include_terminated_users": false, "user_ids": userIDs, "check_date_from": fromDate, "check_date_to": toDate, } if pageToken != "" { reqBody["page_token"] = pageToken } // 发送HTTP请求... // 解析响应respBody... var resp ApiResponse json.Unmarshal(respBody, &resp) allRecords = append(allRecords, resp.Data.UserTaskResults...) if !resp.Data.HasMore { break } pageToken = resp.Data.PageToken // 建议添加一个小延迟,避免请求过快 time.Sleep(100 * time.Millisecond) } return allRecords, nil }实操心得:分批查询用户:直接传入全公司用户的ID列表进行查询,极有可能因为单次请求负载过大而导致API响应超时或失败。更稳健的做法是,先调用部门接口获取部门树和用户列表,然后以部门或小组为单位,分批进行考勤查询。这样每个请求的数据量可控,也便于在出错时定位和重试。
4. 数据处理与输出:从原始JSON到清晰CSV
拿到原始的打卡结果 JSON 数据只是第一步。飞书 API 返回的单个打卡记录结构非常详细,包含了用户信息、多个打卡时段(上班、下班)、每个时段的状态、时间、位置等。我们的目标是将这些嵌套的、冗余的数据“压平”,变成一张简单的表格。
4.1 数据清洗与字段提取
一个典型的原始打卡记录包含如下关键信息:
user_id: 用户IDemployee_no: 工号check_records: 一个数组,包含当天所有的打卡事件(可能多次打卡)。schedules: 一个数组,包含当天的排班信息(如应上班时间、应下班时间)。
我们需要从中提取出对日常管理最有用的信息,通常包括:
- 基础信息:日期、姓名、工号、部门。
- 考勤状态:是否正常、迟到、早退、缺卡。
- 关键时间点:应上班时间、实际上班时间、应下班时间、实际下班时间。
- 时长统计:工作时长、迟到分钟数、早退分钟数。
处理逻辑的复杂性:
- 匹配打卡记录与排班:需要根据
check_records中的check_type(如OnDuty代表上班,OffDuty代表下班)与schedules中的时段进行匹配,以确定哪次打卡对应上班,哪次对应下班。这里要处理多次打卡(比如中午打了卡)的情况,通常取最早的一次作为上班打卡,最晚的一次作为下班打卡。 - 状态判断:对比实际打卡时间和应打卡时间,结合考勤规则(工具内可以预设一个宽容度,如5分钟),判断出
正常、迟到、早退、缺卡等状态。 - 部门信息补全:原始考勤数据里可能只有
user_id。我们需要在工具内部维护一个用户ID到姓名、部门的映射缓存。这可以通过提前调用飞书的GET /contact/v3/users/{user_id}接口批量获取并缓存起来。
4.2 输出格式定制与灵活性
清洗后的数据最终需要输出。CSV 是最通用、最容易被各种软件(Excel、Numbers、数据库、Python pandas)处理的形式。
基础 CSV 字段示例:
日期,工号,姓名,部门,应上班,实际上班,上班状态,应下班,实际下班,下班状态,工作时长(小时),迟到分钟,早退分钟,备注 2023-10-27,10001,张三,技术部,09:00,09:01,迟到,18:00,18:05,正常,8.07,1,0, 2023-10-27,10002,李四,产品部,09:00,08:55,正常,18:00,17:45,早退,8.83,0,15,工具的灵活性设计:一个实用的工具不能只有一种输出格式。我在feishu-inout中加入了以下特性:
- 字段选择:通过命令行参数,允许用户指定只输出他们关心的字段,例如
-fields date,name,check_in_time,status。 - 日期格式化:输出日期时间格式可以定制,适配不同地区的习惯。
- 过滤器:支持在输出前进行简单过滤,例如只输出状态为“迟到”或“缺卡”的记录 (
-filter status=late,absent)。 - 多输出格式:除了 CSV,未来可以扩展支持 JSON Lines (
.jsonl) 格式,方便流式处理;或者直接输出到数据库。
# 假设工具的使用命令示例 ./feishu-inout fetch \ --from 2023-10-01 \ --to 2023-10-31 \ --dept-id od-xxxxxx \ # 指定部门ID --output ./attendance_oct.csv \ --fields date,employee_no,name,dept,check_in,check_out,status \ --filter status=late,absent5. 配置与安全实践
5.1 配置文件管理与敏感信息保护
让用户在每次运行时都通过命令行参数输入App ID和App Secret既麻烦又不安全。因此,一个配置文件是必须的。常见的做法是支持一个 YAML 或 TOML 格式的配置文件,例如config.yaml。
# config.yaml 示例 feishu: app_id: "cli_xxxxxx" app_secret: "xxxxxx-你的AppSecret-xxxxxx" # 重中之重,需保密 # 可选:默认查询的部门根节点ID,如果不指定,则需要在命令中指定 default_department_id: "od-xxxxxxxx" output: default_time_format: "2006-01-02 15:04" # Go语言的时间格式化模板 date_format: "2006-01-02"安全注意事项:
- 绝对不要将
config.yaml提交到版本控制系统(如 Git)。应该在.gitignore文件中加入config.yaml和config.*.yaml。 - 在工具文档中,提供一个
config.example.yaml文件,里面包含所有配置项的结构,但敏感信息用空字符串或占位符代替。 - 鼓励用户通过环境变量来传递最敏感的信息(如 App Secret),这比写在配置文件里更安全。工具可以设计一个优先级:命令行参数 > 环境变量 > 配置文件。
# 通过环境变量使用 export FEISHU_APP_SECRET=your_secret_here ./feishu-inout fetch --from 2023-10-01
5.2 错误处理与日志记录
一个健壮的命令行工具必须有清晰的错误提示和日志记录,方便用户(尤其是非开发者)排查问题。
常见的错误类型及处理:
- API 认证失败:Token 无效或过期。工具应能自动检测并尝试重新获取 Token,如果重试失败,则明确提示用户检查
App ID和App Secret。 - 权限不足:返回
code=99991201等错误码。提示用户去开放平台检查应用权限是否已添加并发布。 - 网络问题或 API 限流:飞书 API 有调用频率限制。工具在遇到限流错误(HTTP 429)时,应自动进行指数退避重试,并在日志中给出提示。
- 参数错误:如日期格式错误、不存在的部门ID。应在参数解析阶段就进行验证,并给出友好的错误信息。
日志记录建议:
- 提供不同日志级别:
DEBUG,INFO,WARN,ERROR。 DEBUG级别可以打印详细的请求和响应信息,用于开发调试。INFO级别记录主要步骤,如“开始获取部门用户列表”、“正在查询2023-10-01的考勤数据”、“成功写入文件 xxx.csv”。ERROR级别记录所有失败信息,并尽可能给出下一步操作建议。- 日志可以输出到标准错误,这样用户可以将标准输出(如 CSV 内容)重定向到文件,而日志信息依然在终端显示。
// 简单的日志实现示例 type Logger struct { level string } func (l *Logger) Info(format string, v ...interface{}) { if l.level == "DEBUG" || l.level == "INFO" { log.Printf("[INFO] "+format, v...) } } func (l *Logger) Error(format string, v ...interface{}) { log.Printf("[ERROR] "+format, v...) }6. 高级功能与扩展场景
6.1 部门树遍历与递归查询
对于中大型公司,组织架构往往有多层。feishu-inout的一个高级功能是支持根据一个父部门ID,递归地获取其下所有子部门的成员,并汇总查询考勤。这需要调用飞书的GET /contact/v3/departments/{department_id}/children和GET /contact/v3/users/find_by_department接口。
实现步骤:
- 递归获取子部门ID列表:从一个根部门开始,调用获取子部门接口,然后对每一个子部门递归调用自身,直到获取所有末级部门的ID。
- 分批获取部门成员:对于每一个部门ID(包括根部门),调用获取部门成员接口。注意该接口可能也分页。
- 用户去重:同一个员工可能同时在多个部门,需要根据
user_id进行去重,避免重复查询其考勤。 - 并发查询考勤:将最终得到的去重后的用户ID列表,分成若干批次(比如每50人一批),利用 Go 的 Goroutine 并发查询不同批次的考勤数据,可以极大缩短整体查询时间。
踩坑记录:API 限制与礼貌性延迟:飞书 API 对并发请求数和每秒查询率有严格限制。盲目高并发请求会导致大量 429 错误。我的经验是,控制并发 Goroutine 的数量(例如不超过5个),并且在每批次请求之间,主动添加一个短暂的延迟(如 200-500 毫秒)。这样既能提升速度,又能保证稳定不超限。
6.2 数据聚合分析与自定义报告
获取到每日明细数据后,工具可以进一步提供简单的聚合分析功能,这比导出 CSV 后再用 Excel 分析又进了一步。
可以内置的聚合功能:
- 部门/个人月度汇总:统计指定月份内,每个员工或部门的迟到次数、早退次数、缺卡次数、平均工作日时长等。
- 异常考勤筛选:快速列出所有在指定时间段内有异常记录(迟到、早退、缺卡)的员工清单。
- 加班时长统计:这是一个更复杂但需求强烈的功能。需要结合排班时间、打卡时间以及公司定义的加班规则(如工作日加班、周末加班、法定节假日加班分别如何计算)来进行初步估算。注意:这通常只能作为参考,最终的加班认定可能涉及审批流程,工具无法完全替代。
自定义报告输出:除了 CSV,工具可以生成一个简单的 Markdown 或 HTML 格式的汇总报告,更适合直接通过飞书机器人发送到群聊进行公示。
# 示例:生成月度部门汇总报告 ./feishu-inout analyze \ --month 2023-10 \ --dept-id od-xxxxxx \ --report-type summary \ --output ./summary_oct.md7. 部署、调度与集成实践
7.1 本地使用与自动化调度
对于个人或小团队,可能只需要每月手动运行一次。但对于需要定期生成报告的场景,自动化调度是必不可少的。
1. 本地定时任务(Mac/Linux):使用系统的crontab可以轻松实现。
# 编辑当前用户的crontab crontab -e # 添加一行,每月1号上午9点执行,生成上个月的考勤数据 0 9 1 * * /path/to/feishu-inout fetch --from $(date -d"-1 month" +\%Y-\%m-01) --to $(date -d"-0 month" +\%Y-\%m-01) --output /path/to/data/attendance_$(date +\%Y\%m).csv 2>&1 | logger -t feishu-inout2. 使用 CI/CD 工具(如 Jenkins, GitHub Actions):这对于需要将考勤数据与其他系统集成的团队更合适。你可以创建一个 Jenkins Pipeline 或 GitHub Actions Workflow,在特定时间触发,运行feishu-inout工具,然后将生成的 CSV 文件上传到内部文件服务器、数据库或发送给指定邮箱。
# GitHub Actions 示例 .github/workflows/fetch-attendance.yml name: Fetch Monthly Attendance on: schedule: - cron: '0 9 1 * *' # 每月1号9点 workflow_dispatch: # 也支持手动触发 jobs: fetch: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Go uses: actions/setup-go@v4 with: { go-version: '1.20' } - name: Build feishu-inout run: go build -o feishu-inout ./cmd/cli - name: Run and Upload env: FEISHU_APP_ID: ${{ secrets.FEISHU_APP_ID }} FEISHU_APP_SECRET: ${{ secrets.FEISHU_APP_SECRET }} run: | ./feishu-inout fetch --from $(date -d"-1 month" +%Y-%m-01) --to $(date +%Y-%m-01) --output attendance.csv - name: Upload Artifact uses: actions/upload-artifact@v3 with: { name: attendance-data, path: attendance.csv }7.2 与企业内部系统集成
feishu-inout输出的结构化数据,可以成为企业数据流中的一个环节。
集成模式举例:
- 数据仓库/BI 系统:定期运行的脚本将 CSV 文件通过
ETL工具(如 Airflow, Kettle)或直接使用 SQL 的LOAD DATA命令,导入到数据仓库(如 ClickHouse, BigQuery)中。之后,便可以在 BI 工具(如 Tableau, FineBI)中制作丰富的考勤分析看板。 - 薪酬计算系统:在计算月度薪资时,自动化流程调用
feishu-inout获取考勤异常数据(迟到、缺卡),根据公司制度自动计算扣款,并将结果传递给薪酬系统。 - 飞书机器人通知:工具本身可以集成飞书机器人的发送能力,或者在 CI 流程结束后,调用飞书机器人 Webhook,将汇总后的异常考勤情况直接发送到指定的管理群,实现主动提醒。
8. 常见问题与排查指南
在实际使用和分享过程中,我总结了一些最常见的问题和解决方法。
8.1 权限问题与错误码
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
运行工具提示code: 99991201, msg: No permission to access data | 应用未获取相应权限或权限未发布 | 1. 登录飞书开放平台,进入应用详情。 2. 在“权限管理”中,确认已添加 attendance:attendance:readonly等必要权限。3. 在页面底部找到“权限管理”标签,点击“发布”,创建并发布一个新版本。 |
获取 Token 失败,返回app_id or app_secret invalid | App ID或App Secret填写错误 | 1. 检查config.yaml或环境变量中的app_id和app_secret值。2. 前往开放平台应用详情页的“凭证与基础信息”栏目核对。 3. 注意 App Secret重置后,旧 Secret 立即失效。 |
查询考勤时返回user_id not found | 传入的用户ID不存在或已离职 | 检查传入的user_ids参数列表。如果使用了部门查询,可能是该部门下存在已离职且未包含在查询参数include_terminated_users中的用户。可尝试将该参数设为true测试。 |
8.2 数据问题与处理
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 导出的数据中,部分员工整天没有记录 | 该员工在所选日期范围内可能没有排班(如请假、调休、新入职未排班) | 飞书API对于没有排班的日子,不会返回该用户的打卡任务记录。这是正常现象。如需区分,需结合请假等数据进行交叉分析。 |
| 打卡时间与预期不符 | 时区问题或数据处理逻辑有误 | 1. 确认飞书后台设置的考勤组所在时区。 2. 检查工具在处理时间戳时是否正确进行了时区转换。飞书API返回的时间戳通常是毫秒级的 Unix 时间戳,需要转换为本地时间。 |
| 一个人一天有多条打卡记录,如何匹配上下班? | 员工在非上下班时间也打了卡(如中午外出) | 工具内的匹配逻辑需要优化。通常策略是:筛选check_type为OnDuty的记录,取时间最早的一条作为上班打卡;筛选OffDuty记录,取时间最晚的一条作为下班打卡。需要仔细测试这种逻辑是否覆盖所有场景。 |
8.3 性能与稳定性优化
- 查询速度慢:如果公司人数众多,查询一个月的全量数据会非常慢。解决方案是按部门分批、按周或按天分批查询,并利用 Go 的并发能力。但要注意控制并发度,避免触发 API 限流。
- 内存占用高:一次性处理数万条记录并构建大的数据结构可能导致内存激增。对于大数据量,建议采用流式处理:每获取一批数据,就立即清洗并写入到输出文件,然后释放内存,而不是全部加载到内存中再统一处理。
- 网络不稳定:在 HTTP 客户端中设置合理的超时时间(如连接超时、读写超时),并实现重试机制(最好是指数退避重试),以应对临时的网络波动。
开发维护feishu-inout这类工具的过程,实际上是一个不断与真实业务场景和第三方 API 细节“磨合”的过程。最大的收获不是代码本身,而是对飞书考勤数据模型的理解,以及对如何设计一个用户友好、健壮可靠的 CLI 工具的深刻体会。工具的价值在于它确实能节省大量重复劳动的时间,让团队成员能更专注于更有价值的数据分析和决策工作。如果你也面临类似的考勤数据处理需求,不妨尝试基于这个思路构建自己的自动化方案,关键的步骤和避坑点已经在上文详细列出,相信能帮你少走很多弯路。