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_fromcheck_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 statuslate,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 statuslate,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_SECRETyour_secret_here ./feishu-inout fetch --from 2023-10-015.2 错误处理与日志记录一个健壮的命令行工具必须有清晰的错误提示和日志记录方便用户尤其是非开发者排查问题。常见的错误类型及处理API 认证失败Token 无效或过期。工具应能自动检测并尝试重新获取 Token如果重试失败则明确提示用户检查App ID和App Secret。权限不足返回code99991201等错误码。提示用户去开放平台检查应用权限是否已添加并发布。网络问题或 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 21 | 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/checkoutv3 - name: Setup Go uses: actions/setup-gov4 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-artifactv3 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 invalidApp 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 工具的深刻体会。工具的价值在于它确实能节省大量重复劳动的时间让团队成员能更专注于更有价值的数据分析和决策工作。如果你也面临类似的考勤数据处理需求不妨尝试基于这个思路构建自己的自动化方案关键的步骤和避坑点已经在上文详细列出相信能帮你少走很多弯路。