使用飞书javaSDK拉取“成员活跃详情”

一、背景

到了年底,公司要对飞书使用情况进行总结、评比,需要导出飞书使用的数据,但是飞书管理员在后台只能导出最近1个月的数据。找了官方,说无法导出全年,他们紧急上线了1个年终总结的API。但是,我们公司跟他们发布的关注纬度不一样,最后导出数据的任务就掉我头上了。

二、思路

通过查阅官方文档《获取用户维度的用户活跃和功能使用数据》

三、过程

(一)在开发者后台创建应用

打开,创建应用并给予对应的权限。创建过程让初学者有点懵,我选的机器人。

(二)拿到应用凭证

(三)在线测试API

打开之前的《获取用户维度的用户活跃和功能使用数据》 ,点击右下角的"前往API调试台",要先注意"切换应用"和"权限配置"。麻烦的是权限设置需要应用发布,但也没事。

测试成功的样子

(四)复制示例代码

(五)java工程创建

1.pom.xml

我为什么知道依赖什么呢?先看了javaSDK指南

XML 复制代码
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>potato</groupId>
		<artifactId>feishu-integration</artifactId>
		<version>0.0.1-SNAPSHOT</version>
	</parent>
	<artifactId>feishu-integration-members</artifactId>

	<dependencies>
		<!-- 飞书Java SDK核心依赖 -->
		<dependency>
			<groupId>com.larksuite.oapi</groupId>
			<artifactId>oapi-sdk</artifactId>
			<version>2.1.0</version> <!-- 推荐使用最新稳定版,可在Maven中央仓库查询 -->
		</dependency>
		<!-- 可选:日志依赖,便于调试 -->
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>1.7.36</version>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-simple</artifactId>
			<version>1.7.36</version>
			<scope>test</scope>
		</dependency>

	</dependencies>
</project>

2.具体的拉取代码

我的复制了代码后,让豆包完善,我再修改的。我的核心是尽量少使用依赖,导出csv文件,每天请求导出1次,每1万行追加写入1次。注意API对时间要求是跨度不超过31天,每页导出不超过60条,不同API要求不同。

java 复制代码
package cn.potato.feishu;

import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.lark.oapi.Client;
import com.lark.oapi.service.admin.v1.model.AdminUserStat;
import com.lark.oapi.service.admin.v1.model.ListAdminUserStatReq;
import com.lark.oapi.service.admin.v1.model.ListAdminUserStatResp;
import com.lark.oapi.service.admin.v1.model.ListAdminUserStatRespBody;

public class FeishuEmployeeFetcher {
	// 替换为你的应用凭证
	private static final String APP_ID = "你的APP_ID";
	private static final String APP_SECRET = "你的APP_SECRET";
	// 分批阈值:每1万条数据写入一次
	private static final int BATCH_SIZE = 10000;
	// 标记是否是第一次写入(用于判断是否需要写入表头)
	private static boolean isFirstWrite = true;
	// 日期格式化器(适配飞书API日期格式要求)
	private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
	// 全局查询范围:2025-01-01 至 2025-12-20
	private static final LocalDate GLOBAL_START_DATE = LocalDate.of(2025, 1, 1);
	private static final LocalDate GLOBAL_END_DATE = LocalDate.of(2025, 12, 20);

	// 初始化飞书Client
	private static Client initFeishuClient() {
		// 创建Client,自动处理token缓存与刷新
		Client client = Client.newBuilder(APP_ID, APP_SECRET).logReqAtDebug(true)// 开启日志,便于调试
				.build();
		return client;
	}

	/**
	 * 按天分段获取所有数据(核心入口方法)
	 */
	public static void getAllDataByDay() throws Exception {
		Client client = initFeishuClient();
		LocalDate currentDate = GLOBAL_START_DATE;

		// 循环遍历每一天,直到超过全局结束日期
		while (!currentDate.isAfter(GLOBAL_END_DATE)) {
			System.out.println(String.format("====================================="));
			System.out.println(String.format("开始查询【%s】的员工数据", currentDate.format(DATE_FORMATTER)));

			// 获取当天的数据(开始日期和结束日期都是当前天)
			fetchDataBySingleDay(client, currentDate);

			// 日期递增1天,处理下一天
			currentDate = currentDate.plusDays(1);
		}

		System.out.println(String.format("====================================="));
		System.out.println("所有日期数据查询并写入完毕!");
	}

	/**
	 * 根据指定日期获取当天数据并分批写入CSV
	 * @param client 飞书Client
	 * @param targetDate 目标日期(当天)
	 */
	private static void fetchDataBySingleDay(Client client, LocalDate targetDate) throws Exception {
		// 临时缓存批次数据
		List<AdminUserStat> batchAusList = new ArrayList<AdminUserStat>();
		String pageToken = "0";
		boolean hasMore = true;
		String dateStr = targetDate.format(DATE_FORMATTER);

		while (hasMore) {
			System.out.println(String.format("  分页查询中,pageToken:%s", pageToken));
			// 创建请求对象(开始日期和结束日期均为当天,满足API时间范围要求)
			ListAdminUserStatReq req = ListAdminUserStatReq.newBuilder()
					.startDate(dateStr)
					.endDate(dateStr)
					.pageToken(pageToken)
					.pageSize(50)
					.build();
			// 发起请求
			ListAdminUserStatResp resp = client.admin().adminUserStat().list(req);

			// 处理服务端错误
			if (!resp.success()) {
				System.out.println(String.format("  【%s】数据查询失败:code:%s,msg:%s,reqId:%s",
						dateStr, resp.getCode(), resp.getMsg(), resp.getRequestId()));
				return;
			}

			ListAdminUserStatRespBody lausrb = resp.getData();
			AdminUserStat[] aus = lausrb.getItems();
			if (aus != null && aus.length > 0) {
				batchAusList.addAll(Arrays.asList(aus));
				System.out.println(String.format("  本次分页获取到%s条数据,当前批次累计%s条", aus.length, batchAusList.size()));
			} else {
				System.out.println("  本次分页无数据");
			}

			// 判断是否达到批次阈值,达到则写入文件并清空临时列表
			if (batchAusList.size() >= BATCH_SIZE) {
				appendToCsvFile(batchAusList, "d:/1.csv");
				batchAusList.clear(); // 清空临时列表,继续缓存后续数据
				System.out.println("  已写入1万条数据,继续分页查询...");
			}

			// 更新分页信息
			pageToken = lausrb.getPageToken();
			hasMore = lausrb.getHasMore();
		}

		// 处理当天最后一批不足1万条的数据
		if (!batchAusList.isEmpty()) {
			appendToCsvFile(batchAusList, "d:/1.csv");
			batchAusList.clear();
			System.out.println(String.format("  【%s】最后一批数据写入完成", dateStr));
		} else {
			System.out.println(String.format("  【%s】无员工数据", dateStr));
		}
	}

	/**
	 * 追加写入CSV文件(支持分批写入,自动判断是否写入表头)
	 * @param ausList 批次数据列表
	 * @param csvFilePath CSV文件路径
	 */
	public static void appendToCsvFile(List<AdminUserStat> ausList, String csvFilePath) {
		// 1. 定义CSV表头
		String[] headers = { "日期", "User ID", "姓名", "部门", "发送消息数", "创建文件数", "创建日程数", "会议时长(分钟)", "会议数", "创建任务数", "总发件量",
				"总收件量", "对外发件数", "对内发件数", "来自外部收件数", "来自内部收件数" };

		// 2. 以追加模式打开文件(FileOutputStream第二个参数为true表示追加,不覆盖原有数据)
		try (BufferedWriter writer = new BufferedWriter(
				new OutputStreamWriter(new FileOutputStream(csvFilePath, true), StandardCharsets.UTF_8))) {

			// 第一次写入时,先写入表头(后续日期均为追加数据,不重复写表头)
			if (isFirstWrite) {
				writer.write(String.join(",", headers));
				writer.newLine();
				isFirstWrite = false; // 写入后标记为非首次
			}

			// 遍历批次数据,追加写入每行内容
			if (ausList != null && !ausList.isEmpty()) {
				for (AdminUserStat aus : ausList) {
					// 处理字段空值,避免空指针异常;转义双引号,防止破坏CSV格式
					String date = aus.getDate() == null ? "" : aus.getDate().replace("\"", "\"\"");
					String userId = aus.getUserId() == null ? "" : aus.getUserId().replace("\"", "\"\"");
					String userName = aus.getUserName() == null ? "" : aus.getUserName().replace("\"", "\"\"");
					String departmentPath = aus.getDepartmentPath() == null ? "" : aus.getDepartmentPath().replace("\"", "\"\"");
					Long sendMessengerNum = aus.getSendMessengerNum() == null ? 0L : aus.getSendMessengerNum();
					Long createDocsNum = aus.getCreateDocsNum() == null ? 0L : aus.getCreateDocsNum();
					Long createCalNum = aus.getCreateCalNum() == null ? 0L : aus.getCreateCalNum();
					Long vcDuration = aus.getVcDuration() == null ? 0L : aus.getVcDuration();
					Long createTaskNum = aus.getCreateTaskNum() == null ? 0L : aus.getCreateTaskNum();
					Long vcNum = aus.getVcNum() == null ? 0L : aus.getVcNum();
					Long emailSendCount = aus.getEmailSendCount() == null ? 0L : Long.parseLong(aus.getEmailSendCount());
					Long emailReceiveCount = aus.getEmailReceiveCount() == null ? 0L : Long.parseLong(aus.getEmailReceiveCount());
					Long emailSendExtCount = aus.getEmailSendExtCount() == null ? 0L : Long.parseLong(aus.getEmailSendExtCount());
					Long emailReceiveExtCount = aus.getEmailReceiveExtCount() == null ? 0L : Long.parseLong(aus.getEmailReceiveExtCount());
					Long emailSendInCount = aus.getEmailSendInCount() == null ? 0L : Long.parseLong(aus.getEmailSendInCount());
					Long emailReceiveInCount = aus.getEmailReceiveInCount() == null ? 0L : Long.parseLong(aus.getEmailReceiveInCount());

					// 拼接字段值,用引号包裹避免逗号/换行符破坏CSV格式
					String line = String.format(
							"\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"",
							date, userId, userName, departmentPath, sendMessengerNum,
							createDocsNum, createCalNum, vcDuration, createTaskNum,
							vcNum, emailSendCount, emailReceiveCount,
							emailSendExtCount, emailReceiveExtCount, emailSendInCount,
							emailReceiveInCount);
					writer.write(line);
					writer.newLine();
				}
			}

			System.out.println(String.format("  成功追加%s条数据到CSV文件:%s", ausList.size(), csvFilePath));
		} catch (Exception e) {
			e.printStackTrace();
			throw new RuntimeException("追加写入CSV文件失败:" + e.getMessage());
		}
	}

	// 主方法测试
	public static void main(String[] args) throws Exception {
		getAllDataByDay();
	}
}

四、感受

1.总体来说,飞书的API比较完。

2.代码注释给力,开发者友好。

3.示例代码与依赖文档衔接友好,示例代码中有文档链接

4.在线API测试没有问题,先下就可以运行。

相关推荐
天远云服16 小时前
Go 语言高并发实战:批量清洗天远借贷行为验证API (JRZQ8203) 的时间序列数据
大数据·api
helloCat21 小时前
记录CI/CD自动化上传AppGallery遇到的坑
android·前端·api
天空属于哈夫克31 天前
外部群公告的动态更新算法
自动化·企业微信·api·rpa
驱动探索者2 天前
[缩略语大全]之[编译器]篇
计算机·状态模式·飞书·编译器
天空属于哈夫克32 天前
外部群公告内容的动态生成与格式化注入
自动化·企业微信·api·rpa
工程师0072 天前
C# 调用 Win32 API
开发语言·c#·api·win32
天空属于哈夫克33 天前
关键词触发自动回复的精准匹配模型
企业微信·api·rpa
骚戴3 天前
在科研与项目开发中:如何高效调用大语言模型(LLM)API
人工智能·语言模型·自然语言处理·大模型·llm·api
Teable任意门互动3 天前
飞书多维表格vsTeable 如何选?把握“内外兼修”是关键决策点
运维·自动化·飞书·数据库开发·wps