# 飞书表格助手操作文档
## 目录
- [1. 项目概述](#1-项目概述)
- [2. 快速开始](#2-快速开始)
- [3. 核心概念](#3-核心概念)
- [4. 注解方式使用(实体类)](#4-注解方式使用实体类)
- [5. Map 配置方式使用](#5-map-配置方式使用)
- [6. 高级特性](#6-高级特性)
- [7. 实际应用场景](#7-实际应用场景)
- [8. API 参考](#8-api-参考)
- [9. 最佳实践](#9-最佳实践)
- [10. 常见问题(FAQ)](#10-常见问题faq)
---
## 1. 项目概述
### 1.1 项目简介
`feishu-table-helper` 是一个简化飞书表格操作的 Java 库。通过使用注解或 Map 配置,开发者可以轻松地将 Java 实体类映射到飞书表格,实现表格的自动创建、数据读取和写入操作,大大简化了与飞书表格 API 的交互。
### 1.2 核心能力
- **注解驱动**:使用 `@TableProperty` 和 `@TableConf` 注解将实体类字段映射到表格列
- **自动创建表格**:根据实体类结构自动创建飞书表格和设置表头
- **数据读取**:从飞书表格读取数据并映射到实体类对象
- **数据写入**:将实体类对象写入飞书表格,支持新增和更新操作(Upsert)
- **灵活配置**:支持自定义表格样式、单元格格式、下拉选项等
- **Map 方式**:无需定义实体类,直接使用 Map 配置操作表格
- **多层级表头**:支持复杂的多层级表头结构
- **分组表格**:支持分组字段,实现分组数据的创建和读取
### 1.3 适用场景
- **数据同步**:将业务系统的数据同步到飞书表格
- **报表生成**:自动生成和更新飞书表格报表
- **数据采集**:通过飞书表格收集数据并回读到系统
- **数据展示**:将系统数据以表格形式展示给非技术人员
- **协作办公**:团队成员通过飞书表格协作编辑数据
### 1.4 版本信息
- **当前版本**:0.0.5
- **最低 Java 版本**:Java 8
- **主要依赖**:
- 飞书开放平台 SDK (oapi-sdk) v2.4.21
- OkHttp v4.12.0
- Gson v2.8.9
---
## 2. 快速开始
### 2.1 环境准备
#### Maven 依赖
```xml
cn.isliu
feishu-table-helper
0.0.5
```
#### Gradle 依赖
```gradle
implementation 'cn.isliu:feishu-table-helper:0.0.5'
```
### 2.2 初始化配置
在使用飞书表格助手之前,需要先初始化飞书客户端。你需要从飞书开放平台获取应用的 `App ID` 和 `App Secret`。
```java
import cn.isliu.core.client.FsClient;
// 初始化配置
try (FsClient fsClient = FsClient.getInstance()) {
fsClient.initializeClient("your_app_id", "your_app_secret");
// 后续的表格操作都在这个 try-with-resources 块中进行
// 或者在整个应用生命周期中保持 FsClient 实例
}
```
**重要提示**:
- `FsClient.getInstance()` 返回的是单例实例
- 建议使用 try-with-resources 确保资源正确释放
- 如果需要在应用生命周期中保持连接,可以在应用启动时初始化一次
### 2.3 第一个示例
让我们通过一个简单的员工信息管理示例来快速上手:
#### 步骤 1:创建实体类
```java
import cn.isliu.core.BaseEntity;
import cn.isliu.core.annotation.TableConf;
import cn.isliu.core.annotation.TableProperty;
@TableConf(headLine = 2, titleRow = 1, enableDesc = true)
public class Employee extends BaseEntity {
@TableProperty(value = "员工编号", order = 0, desc = "员工编号不超过20个字符")
private String employeeId;
@TableProperty(value = "姓名", order = 1, desc = "员工姓名不超过20个字符")
private String name;
@TableProperty(value = "部门", order = 2, desc = "员工部门")
private String department;
@TableProperty(value = "邮箱", order = 3, desc = "员工邮箱")
private String email;
// getters and setters
public String getEmployeeId() { return employeeId; }
public void setEmployeeId(String employeeId) { this.employeeId = employeeId; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDepartment() { return department; }
public void setDepartment(String department) { this.department = department; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
```
#### 步骤 2:创建表格
```java
import cn.isliu.FsHelper;
// 假设你已经初始化了 FsClient
String spreadsheetToken = "your_spreadsheet_token"; // 电子表格Token
String sheetId = FsHelper.create("员工表", spreadsheetToken, Employee.class);
System.out.println("创建的工作表ID: " + sheetId);
```
#### 步骤 3:写入数据
```java
List employees = new ArrayList<>();
Employee emp = new Employee();
emp.setEmployeeId("E001");
emp.setName("张三");
emp.setDepartment("技术部");
emp.setEmail("zhangsan@company.com");
employees.add(emp);
Employee emp2 = new Employee();
emp2.setEmployeeId("E002");
emp2.setName("李四");
emp2.setDepartment("产品部");
emp2.setEmail("lisi@company.com");
employees.add(emp2);
FsHelper.write(sheetId, spreadsheetToken, employees);
```
#### 步骤 4:读取数据
```java
List employees = FsHelper.read(sheetId, spreadsheetToken, Employee.class);
employees.forEach(emp -> {
System.out.println(emp.getName() + " - " + emp.getEmail());
System.out.println("唯一ID: " + emp.getUniqueId());
System.out.println("行号: " + emp.getRow());
});
```
---
## 3. 核心概念
### 3.1 两种使用方式
#### 方式一:注解方式(实体类)
使用 `@TableConf` 和 `@TableProperty` 注解在实体类上定义表格结构,适用于:
- 表格结构相对固定
- 需要类型安全
- 需要 IDE 自动补全
**优点**:
- 类型安全
- IDE 支持好
- 代码可读性强
**缺点**:
- 需要定义实体类
- 结构变更需要重新编译
#### 方式二:Map 配置方式
使用 `MapSheetConfig`、`MapTableConfig` 和 `MapFieldDefinition` 配置表格,适用于:
- 动态字段
- 表格结构经常变化
- 不需要定义实体类
**优点**:
- 灵活,支持动态配置
- 无需定义实体类
- 适合配置驱动的场景
**缺点**:
- 类型安全性较低
- 需要手动处理类型转换
### 3.2 重要术语
#### spreadsheetToken(电子表格 Token)
飞书电子表格的唯一标识符。你可以在飞书表格的 URL 中找到,例如:
```
https://example.feishu.cn/sheets/shtcnxxxxxxxxxxxxxxxxx
^^^^^^^^^^^^^^^^^^^^^^^^
这就是 spreadsheetToken
```
#### sheetId(工作表 ID)
电子表格中的单个工作表的唯一标识符。通过 `FsHelper.create()` 方法创建表格后会返回这个 ID。
#### titleRow(标题行)
表格中表头所在的行号(从 1 开始计数)。例如:
- `titleRow = 1` 表示表头在第 1 行
- `titleRow = 3` 表示表头在第 3 行
#### headLine(数据起始行 - 1)
表格中数据开始的行号(从 1 开始计数)。通常等于 `titleRow + 1`,但如果有描述行,可能需要加 2。
**示例**:
```
行1: [表头分组1] [表头分组2] <- 表头分组行(可选)
行2: [列名1] [列名2] <- 标题行(titleRow = 2)
行3: [描述1] [描述2] <- 描述行(可选,enableDesc = true)
行4: [数据1] [数据2] <- 数据起始行(headLine = 3)
```
#### uniqueId(唯一标识)
系统根据唯一键字段自动计算的唯一标识符。用于:
- 数据去重
- Upsert 模式中的数据匹配
- 数据更新
#### Upsert 模式
Upsert(Update + Insert)模式是一种数据写入策略:
- **默认开启**(`upsert = true`):根据唯一键匹配,如果数据已存在则更新,不存在则追加
- **关闭**(`upsert = false`):不匹配唯一键,所有数据直接追加到表格末尾
---
## 4. 注解方式使用(实体类)
### 4.1 实体类定义
#### @TableConf 注解
`@TableConf` 注解用于配置整个表格的全局属性,必须放在实体类上。
**主要参数**:
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `uniKeys` | String[] | {} | 唯一键字段名数组,用于数据更新和去重 |
| `headLine` | int | 1 | 数据起始行号 |
| `titleRow` | int | 1 | 标题行行号 |
| `enableCover` | boolean | false | 是否覆盖已存在的数据 |
| `isText` | boolean | false | 是否设置表格为纯文本格式 |
| `enableDesc` | boolean | false | 是否启用字段描述行 |
| `headFontColor` | String | "#000000" | 表头字体颜色(十六进制) |
| `headBackColor` | String | "#cccccc" | 表头背景颜色(十六进制) |
| `upsert` | boolean | true | 是否启用 Upsert 模式 |
**示例**:
```java
@TableConf(
headLine = 4,
titleRow = 3,
enableDesc = true,
uniKeys = {"employeeId"},
headFontColor = "#ffffff",
headBackColor = "#1890ff"
)
public class Employee extends BaseEntity {
// ...
}
```
#### @TableProperty 注解
`@TableProperty` 注解用于配置实体类字段与表格列的映射关系,必须放在字段上。
**主要参数**:
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `value` | String[] | {} | 表格列名,支持多层级表头 |
| `desc` | String | "" | 字段描述 |
| `order` | int | Integer.MAX_VALUE | 字段排序顺序,数值越小越靠前 |
| `type` | TypeEnum | TEXT | 字段类型(文本、单选、多选等) |
| `enumClass` | Class | BaseEnum.class | 枚举类(用于单选/多选) |
| `fieldFormatClass` | Class | FieldValueProcess.class | 字段格式化处理类 |
| `optionsClass` | Class | OptionsValueProcess.class | 选项处理类 |
**示例**:
```java
// 单层级表头
@TableProperty(value = "姓名", order = 0)
private String name;
// 多层级表头
@TableProperty(value = {"ID", "员工信息", "员工编号"}, order = 0)
private String employeeId;
// 单选字段
@TableProperty(value = "状态", order = 1, type = TypeEnum.SINGLE_SELECT, enumClass = StatusEnum.class)
private String status;
// 带描述的字段
@TableProperty(value = "邮箱", order = 2, desc = "员工邮箱地址")
private String email;
```
#### BaseEntity 基类
所有使用注解方式的实体类都应该继承 `BaseEntity`,它提供了以下属性:
- `uniqueId`:唯一标识符(自动生成)
- `row`:数据所在的行号
- `rowData`:原始行数据(Map格式)
```java
public class Employee extends BaseEntity {
// 你的字段定义
}
```
### 4.2 创建表格
#### 基础创建
使用 `FsHelper.create()` 方法创建表格:
```java
String sheetId = FsHelper.create("员工表", spreadsheetToken, Employee.class);
```
这个方法会:
1. 创建新的工作表
2. 根据实体类注解生成表头
3. 设置表格样式(字体、背景色等)
4. 合并多层级表头的单元格
5. 设置下拉选项(如果配置了枚举类)
6. 设置字段描述(如果启用了 `enableDesc`)
#### 使用 SheetBuilder 高级配置
`SheetBuilder` 提供了更灵活的配置方式,支持链式调用:
```java
String sheetId = FsHelper.createBuilder("员工表", spreadsheetToken, Employee.class)
.includeFields("employeeId", "name", "email") // 只包含指定字段
.excludeFields("department") // 排除指定字段
.customProperties(customProps) // 自定义属性
.build();
```
**SheetBuilder 主要方法**:
- `includeFields(String... fields)`:只包含指定的字段
- `excludeFields(String... fields)`:排除指定的字段
- `customProperties(Map props)`:自定义属性,用于传递给选项处理类等
#### 多层级表头
多层级表头通过 `@TableProperty` 的 `value` 参数配置,使用字符串数组:
```java
@TableConf(headLine = 4, titleRow = 3)
public class Employee extends BaseEntity {
// 三级表头:ID -> 员工信息 -> 员工编号
@TableProperty(value = {"ID", "员工信息", "员工编号"}, order = 0)
private String employeeId;
// 三级表头:ID -> 员工信息 -> 姓名
@TableProperty(value = {"ID", "员工信息", "姓名"}, order = 1)
private String name;
// 二级表头:联系方式 -> 邮箱
@TableProperty(value = {"联系方式", "邮箱"}, order = 2)
private String email;
// 单级表头:部门
@TableProperty(value = "部门", order = 3)
private String department;
}
```
**表头结构示例**:
```
行1: [ ID ] [ ] [ ]
行2: [ 员工信息 ] [联系方式] [ ]
行3: [员工编号] [ 姓名 ] [ 邮箱 ] [ 部门 ]
行4: [数据... ] [数据...] [数据...] [数据...]
```
系统会自动合并相同值的单元格。
#### 分组表格
分组表格允许多个字段组,每个组包含相同的字段集合。例如,在问卷调查中,可能需要为多个受访者记录相同的信息。
**注意**:分组表格功能主要在 Map 配置方式中使用,注解方式通过多层级表头实现类似效果。
### 4.3 写入数据
#### 基础写入
使用 `FsHelper.write()` 方法写入数据:
```java
List employees = new ArrayList<>();
// ... 准备数据
FsHelper.write(sheetId, spreadsheetToken, employees);
```
**写入逻辑**:
1. 根据 `uniKeys` 计算每条数据的 `uniqueId`
2. 在现有数据中查找匹配的 `uniqueId`
3. 如果找到(且 `upsert = true`),更新对应行
4. 如果没找到,追加到表格末尾
#### 使用 WriteBuilder 高级配置
```java
FsHelper.writeBuilder(sheetId, spreadsheetToken, employees)
.ignoreUniqueFields("updateTime") // 计算唯一ID时忽略指定字段
.build();
```
**WriteBuilder 主要方法**:
- `ignoreUniqueFields(String... fields)`:计算唯一ID时忽略的字段
#### Upsert 模式
Upsert 模式是默认启用的,可以通过 `@TableConf(upsert = false)` 关闭。
**示例:启用 Upsert 模式**
```java
@TableConf(uniKeys = {"employeeId"}, upsert = true)
public class Employee extends BaseEntity {
@TableProperty(value = "员工编号", order = 0)
private String employeeId;
// ...
}
// 写入数据
Employee emp1 = new Employee();
emp1.setEmployeeId("E001");
emp1.setName("张三");
employees.add(emp1);
FsHelper.write(sheetId, spreadsheetToken, employees);
// 如果表格中已存在 employeeId = "E001" 的记录,会更新该记录
// 如果不存在,会追加新记录
```
**示例:关闭 Upsert 模式(纯追加)**
```java
@TableConf(uniKeys = {"employeeId"}, upsert = false)
public class Employee extends BaseEntity {
// ...
}
// 写入数据
FsHelper.write(sheetId, spreadsheetToken, employees);
// 所有数据都会追加到表格末尾,不会检查是否已存在
```
#### 图片上传
支持将图片上传到飞书表格单元格中:
```java
import cn.isliu.core.FileData;
import cn.isliu.core.enums.FileType;
public class Product extends BaseEntity {
@TableProperty(value = "产品图片", order = 0)
private FileData image;
// getters and setters
}
// 写入数据
Product product = new Product();
FileData fileData = new FileData();
fileData.setFileName("product.jpg");
fileData.setImageData(imageBytes); // 图片的字节数组
fileData.setFileType(FileType.IMAGE.getType());
product.setImage(fileData);
List products = new ArrayList<>();
products.add(product);
FsHelper.write(sheetId, spreadsheetToken, products);
```
### 4.4 读取数据
#### 基础读取
使用 `FsHelper.read()` 方法读取数据:
```java
List employees = FsHelper.read(sheetId, spreadsheetToken, Employee.class);
```
**读取逻辑**:
1. 读取表格数据
2. 根据 `titleRow` 识别表头
3. 从 `headLine` 开始读取数据行
4. 将数据映射到实体类对象
5. 自动计算 `uniqueId` 并设置到 `BaseEntity`
#### 使用 ReadBuilder 高级配置
```java
List employees = FsHelper.readBuilder(sheetId, spreadsheetToken, Employee.class)
.ignoreUniqueFields("updateTime") // 计算唯一ID时忽略指定字段
.build();
```
**ReadBuilder 主要方法**:
- `ignoreUniqueFields(String... fields)`:计算唯一ID时忽略的字段
**读取后的对象属性**:
```java
for (Employee emp : employees) {
// 业务字段
System.out.println(emp.getName());
// BaseEntity 提供的属性
System.out.println("唯一ID: " + emp.getUniqueId());
System.out.println("行号: " + emp.getRow());
System.out.println("原始数据: " + emp.getRowData());
}
```
---
## 5. Map 配置方式使用
Map 配置方式适用于不需要定义实体类的场景,提供了更大的灵活性。
### 5.1 MapFieldDefinition 字段定义
`MapFieldDefinition` 用于定义单个字段的所有属性。
#### 快速创建方法
```java
import cn.isliu.core.config.MapFieldDefinition;
// 创建文本字段
MapFieldDefinition field1 = MapFieldDefinition.text("字段名", 0);
// 创建文本字段(带描述)
MapFieldDefinition field2 = MapFieldDefinition.text("字段名", 0, "字段描述");
// 创建单选字段
MapFieldDefinition field3 = MapFieldDefinition.singleSelect("状态", 1, "启用", "禁用");
// 创建多选字段
MapFieldDefinition field4 = MapFieldDefinition.multiSelect("标签", 2, "标签1", "标签2", "标签3");
// 使用枚举类创建单选字段
MapFieldDefinition field5 = MapFieldDefinition.singleSelectWithEnum("状态", 1, StatusEnum.class);
```
#### 使用 Builder 创建
```java
MapFieldDefinition field = MapFieldDefinition.builder()
.fieldName("字段名")
.order(0)
.type(TypeEnum.TEXT)
.description("字段描述")
.required(true)
.defaultValue("默认值")
.build();
```
#### 字段类型
支持的类型(`TypeEnum`):
- `TEXT`:文本
- `SINGLE_SELECT`:单选
- `MULTI_SELECT`:多选
- `NUMBER`:数字
- `DATE`:日期
- `TEXT_FILE`:文本文件
- `MULTI_TEXT`:多个文本(逗号分割)
- `TEXT_URL`:文本链接
### 5.2 MapSheetConfig 表格配置
`MapSheetConfig` 用于配置表格创建时的属性。
#### 基础配置
```java
import cn.isliu.core.config.MapSheetConfig;
MapSheetConfig config = MapSheetConfig.sheetBuilder()
.titleRow(2) // 标题行
.headLine(3) // 数据起始行
.headStyle("#ffffff", "#000000") // 字体颜色,背景颜色
.isText(false) // 是否纯文本格式
.enableDesc(true) // 是否启用描述
.addField(MapFieldDefinition.text("字段1", 0))
.addField(MapFieldDefinition.text("字段2", 1))
.build();
```
#### 批量添加字段
```java
List fields = new ArrayList<>();
fields.add(MapFieldDefinition.text("字段1", 0));
fields.add(MapFieldDefinition.text("字段2", 1));
MapSheetConfig config = MapSheetConfig.sheetBuilder()
.addFields(fields) // 批量添加
.build();
// 或者使用可变参数
MapSheetConfig config2 = MapSheetConfig.sheetBuilder()
.addFields(
MapFieldDefinition.text("字段1", 0),
MapFieldDefinition.text("字段2", 1)
)
.build();
```
### 5.3 MapTableConfig 读写配置
`MapTableConfig` 用于配置数据读写时的属性。
```java
import cn.isliu.core.config.MapTableConfig;
MapTableConfig config = MapTableConfig.builder()
.titleRow(2) // 标题行
.headLine(3) // 数据起始行
.addUniKeyName("字段1") // 添加唯一键
.enableCover(false) // 是否覆盖
.upsert(true) // 是否启用 Upsert
.ignoreNotFound(false) // 是否忽略未找到的数据
.build();
```
### 5.4 创建表格
#### 使用 createMapSheet
```java
MapSheetConfig config = MapSheetConfig.sheetBuilder()
.titleRow(2)
.headLine(3)
.addField(MapFieldDefinition.text("姓名", 0))
.addField(MapFieldDefinition.text("年龄", 1))
.build();
String sheetId = FsHelper.createMapSheet("表格名称", spreadsheetToken, config);
```
#### 使用 MapSheetBuilder
```java
String sheetId = FsHelper.createMapSheetBuilder("表格名称", spreadsheetToken)
.titleRow(2)
.headLine(3)
.headStyle("#ffffff", "#000000")
.isText(false)
.enableDesc(true)
.addField(MapFieldDefinition.text("姓名", 0))
.addField(MapFieldDefinition.text("年龄", 1))
.build();
```
**MapSheetBuilder 主要方法**:
- `titleRow(int row)`:设置标题行
- `headLine(int line)`:设置数据起始行
- `headStyle(String fontColor, String backColor)`:设置表头样式
- `isText(boolean isText)`:是否纯文本格式
- `enableDesc(boolean enable)`:是否启用描述
- `addField(MapFieldDefinition field)`:添加单个字段
- `addFields(List fields)`:批量添加字段
- `addFields(MapFieldDefinition... fields)`:批量添加字段(可变参数)
- `fields(List fields)`:设置所有字段(覆盖现有)
- `groupFields(String... groupFields)`:设置分组字段(用于分组表格)
### 5.5 写入数据
#### 使用 writeMap
```java
List