From 7bac7917a01573673f9e35782a59c32ea83f0f18 Mon Sep 17 00:00:00 2001 From: liushuang Date: Wed, 13 Aug 2025 16:50:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20=E6=96=B0=E5=A2=9E=E6=A0=B8?= =?UTF-8?q?=E5=BF=83=E5=8A=9F=E8=83=BD=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 39 + pom.xml | 34 + src/main/java/cn/isliu/FsHelper.java | 136 ++ src/main/java/cn/isliu/core/AddSheet.java | 42 + src/main/java/cn/isliu/core/BaseEntity.java | 14 + src/main/java/cn/isliu/core/Cell.java | 72 + src/main/java/cn/isliu/core/CopySheet.java | 43 + src/main/java/cn/isliu/core/DeleteSheet.java | 53 + src/main/java/cn/isliu/core/FsTableData.java | 64 + .../java/cn/isliu/core/GridProperties.java | 70 + src/main/java/cn/isliu/core/Merge.java | 77 + src/main/java/cn/isliu/core/MergedCell.java | 55 + src/main/java/cn/isliu/core/Properties.java | 65 + src/main/java/cn/isliu/core/Reply.java | 64 + src/main/java/cn/isliu/core/Sheet.java | 97 + src/main/java/cn/isliu/core/SheetMeta.java | 40 + src/main/java/cn/isliu/core/TableData.java | 38 + src/main/java/cn/isliu/core/TableRow.java | 43 + src/main/java/cn/isliu/core/ValueRange.java | 76 + src/main/java/cn/isliu/core/ValuesBatch.java | 76 + .../isliu/core/annotation/TableProperty.java | 28 + .../cn/isliu/core/client/FeishuApiClient.java | 99 + .../cn/isliu/core/client/FeishuClient.java | 266 ++ .../java/cn/isliu/core/config/FsConfig.java | 56 + .../core/converters/FieldValueProcess.java | 11 + .../isliu/core/converters/FileUrlProcess.java | 128 + .../core/converters/OptionsValueProcess.java | 6 + .../java/cn/isliu/core/enums/BaseEnum.java | 53 + .../java/cn/isliu/core/enums/TypeEnum.java | 41 + .../core/exception/FsHelperException.java | 12 + .../java/cn/isliu/core/pojo/ApiResponse.java | 80 + .../cn/isliu/core/pojo/FieldProperty.java | 65 + .../isliu/core/service/CustomCellService.java | 2164 +++++++++++++++++ .../service/CustomDataValidationService.java | 1094 +++++++++ .../core/service/CustomDimensionService.java | 1083 +++++++++ .../CustomProtectedDimensionService.java | 453 ++++ .../core/service/CustomSheetService.java | 870 +++++++ .../core/service/CustomValueService.java | 1672 +++++++++++++ .../cn/isliu/core/utils/ConvertFieldUtil.java | 235 ++ .../java/cn/isliu/core/utils/FileUtil.java | 121 + .../java/cn/isliu/core/utils/FsApiUtil.java | 488 ++++ .../cn/isliu/core/utils/FsClientUtil.java | 35 + .../java/cn/isliu/core/utils/FsTableUtil.java | 323 +++ .../cn/isliu/core/utils/GenerateUtil.java | 434 ++++ .../java/cn/isliu/core/utils/JSONUtil.java | 49 + .../cn/isliu/core/utils/PropertyUtil.java | 336 +++ .../java/cn/isliu/core/utils/StringUtil.java | 128 + 47 files changed, 11528 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/cn/isliu/FsHelper.java create mode 100644 src/main/java/cn/isliu/core/AddSheet.java create mode 100644 src/main/java/cn/isliu/core/BaseEntity.java create mode 100644 src/main/java/cn/isliu/core/Cell.java create mode 100644 src/main/java/cn/isliu/core/CopySheet.java create mode 100644 src/main/java/cn/isliu/core/DeleteSheet.java create mode 100644 src/main/java/cn/isliu/core/FsTableData.java create mode 100644 src/main/java/cn/isliu/core/GridProperties.java create mode 100644 src/main/java/cn/isliu/core/Merge.java create mode 100644 src/main/java/cn/isliu/core/MergedCell.java create mode 100644 src/main/java/cn/isliu/core/Properties.java create mode 100644 src/main/java/cn/isliu/core/Reply.java create mode 100644 src/main/java/cn/isliu/core/Sheet.java create mode 100644 src/main/java/cn/isliu/core/SheetMeta.java create mode 100644 src/main/java/cn/isliu/core/TableData.java create mode 100644 src/main/java/cn/isliu/core/TableRow.java create mode 100644 src/main/java/cn/isliu/core/ValueRange.java create mode 100644 src/main/java/cn/isliu/core/ValuesBatch.java create mode 100644 src/main/java/cn/isliu/core/annotation/TableProperty.java create mode 100644 src/main/java/cn/isliu/core/client/FeishuApiClient.java create mode 100644 src/main/java/cn/isliu/core/client/FeishuClient.java create mode 100644 src/main/java/cn/isliu/core/config/FsConfig.java create mode 100644 src/main/java/cn/isliu/core/converters/FieldValueProcess.java create mode 100644 src/main/java/cn/isliu/core/converters/FileUrlProcess.java create mode 100644 src/main/java/cn/isliu/core/converters/OptionsValueProcess.java create mode 100644 src/main/java/cn/isliu/core/enums/BaseEnum.java create mode 100644 src/main/java/cn/isliu/core/enums/TypeEnum.java create mode 100644 src/main/java/cn/isliu/core/exception/FsHelperException.java create mode 100644 src/main/java/cn/isliu/core/pojo/ApiResponse.java create mode 100644 src/main/java/cn/isliu/core/pojo/FieldProperty.java create mode 100644 src/main/java/cn/isliu/core/service/CustomCellService.java create mode 100644 src/main/java/cn/isliu/core/service/CustomDataValidationService.java create mode 100644 src/main/java/cn/isliu/core/service/CustomDimensionService.java create mode 100644 src/main/java/cn/isliu/core/service/CustomProtectedDimensionService.java create mode 100644 src/main/java/cn/isliu/core/service/CustomSheetService.java create mode 100644 src/main/java/cn/isliu/core/service/CustomValueService.java create mode 100644 src/main/java/cn/isliu/core/utils/ConvertFieldUtil.java create mode 100644 src/main/java/cn/isliu/core/utils/FileUtil.java create mode 100644 src/main/java/cn/isliu/core/utils/FsApiUtil.java create mode 100644 src/main/java/cn/isliu/core/utils/FsClientUtil.java create mode 100644 src/main/java/cn/isliu/core/utils/FsTableUtil.java create mode 100644 src/main/java/cn/isliu/core/utils/GenerateUtil.java create mode 100644 src/main/java/cn/isliu/core/utils/JSONUtil.java create mode 100644 src/main/java/cn/isliu/core/utils/PropertyUtil.java create mode 100644 src/main/java/cn/isliu/core/utils/StringUtil.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..acb7754 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +.idea + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..ff0be76 --- /dev/null +++ b/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + cn.isliu + feishu-table-helper + 1.0-SNAPSHOT + + + 8 + 8 + UTF-8 + + + + + com.larksuite.oapi + oapi-sdk + 2.4.21 + + + com.squareup.okhttp3 + okhttp + 4.12.0 + + + com.google.code.gson + gson + 2.8.9 + + + \ No newline at end of file diff --git a/src/main/java/cn/isliu/FsHelper.java b/src/main/java/cn/isliu/FsHelper.java new file mode 100644 index 0000000..db689af --- /dev/null +++ b/src/main/java/cn/isliu/FsHelper.java @@ -0,0 +1,136 @@ +package cn.isliu; + +import cn.isliu.core.BaseEntity; +import cn.isliu.core.FsTableData; +import cn.isliu.core.Sheet; +import cn.isliu.core.config.FsConfig; +import cn.isliu.core.pojo.FieldProperty; +import cn.isliu.core.service.CustomValueService; +import cn.isliu.core.utils.*; +import com.google.gson.JsonObject; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +public class FsHelper { + + public static Boolean create(String sheetName, String spreadsheetToken, Class clazz) { + Map fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz); + List headers = PropertyUtil.getHeaders(fieldsMap); + + // 1、创建sheet + String sheetId = FsApiUtil.createSheet(sheetName, FsClientUtil.getFeishuClient(), spreadsheetToken); + + // 2 添加表头数据 + FsApiUtil.putValues(spreadsheetToken, FsTableUtil.getHeadTemplateBuilder(sheetId, headers), FsClientUtil.getFeishuClient()); + + // 3 设置表格样式 + FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, headers.size()), sheetId, FsClientUtil.getFeishuClient(), spreadsheetToken); + + // 4 设置单元格为文本格式 + if (FsConfig.CELL_TEXT) { + String column = FsTableUtil.getColumnNameByNuNumber(headers.size()); + FsApiUtil.setCellType(sheetId, "@", "A1", column + 200, FsClientUtil.getFeishuClient(), spreadsheetToken); + } + + // 5 设置表格下拉 + FsTableUtil.setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId); + return true; + } + + + public static List read(String sheetId, String spreadsheetToken, Class clazz) { + List results = new ArrayList<>(); + Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, FsClientUtil.getFeishuClient(), spreadsheetToken); + List fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken); + + Map fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz); + List fieldPathList = fieldsMap.values().stream().map(FieldProperty::getField).collect(Collectors.toList()); + + fsTableDataList.forEach(tableData -> { + Object data = tableData.getData(); + if (data instanceof HashMap) { + JsonObject jsonObject = JSONUtil.convertHashMapToJsonObject((HashMap) data); + Map dataMap = ConvertFieldUtil.convertPositionToField(jsonObject, fieldsMap); + T t = GenerateUtil.generateInstance(fieldPathList, clazz, dataMap); + if (t instanceof BaseEntity) { + ((BaseEntity) t).setUniqueId(tableData.getUniqueId()); + } + results.add(t); + } + }); + return results; + } + + public static Object write(String sheetId, String spreadsheetToken, List dataList) { + if (dataList.isEmpty()) { + return null; + } + + Class aClass = dataList.get(0).getClass(); + Map fieldsMap = PropertyUtil.getTablePropertyFieldsMap(aClass); + + Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, FsClientUtil.getFeishuClient(), spreadsheetToken); + List fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken); + Map currTableRowMap = fsTableDataList.stream().collect(Collectors.toMap(FsTableData::getUniqueId, FsTableData::getRow)); + + final Integer[] row = {0}; + fsTableDataList.forEach(fsTableData -> { + if (fsTableData.getRow() > row[0]) { + row[0] = fsTableData.getRow(); + } + }); + + Map titlePostionMap = FsTableUtil.getTitlePostionMap(sheet, spreadsheetToken); + + Map fieldMap = new HashMap<>(); + fieldsMap.forEach((field, fieldProperty) -> fieldMap.put(field, fieldProperty.getField())); + + // 初始化批量插入对象 + CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder = CustomValueService.ValueRequest.batchPutValues(); + + AtomicInteger rowCount = new AtomicInteger(row[0] + 1); + + for (T data : dataList) { + Map values = GenerateUtil.getFieldValue(data, fieldMap); + + String uniqueId = GenerateUtil.getUniqueId(data); + + AtomicReference rowNum = new AtomicReference<>(currTableRowMap.get(uniqueId)); + if (uniqueId != null && rowNum.get() != null) { + rowNum.set(rowNum.get() + 1); + values.forEach((field, fieldValue) -> { + if (!FsConfig.isCover && fieldValue == null) { + return; + } + + String position = titlePostionMap.get(field); + resultValuesBuilder.addRange(sheetId, position + rowNum.get(), position + rowNum.get()) + .addRow(fieldValue instanceof List ? GenerateUtil.getFieldValueList(fieldValue) : fieldValue); + }); + } else { + int rowCou = rowCount.incrementAndGet(); + values.forEach((field, fieldValue) -> { + if (!FsConfig.isCover && fieldValue == null) { + return; + } + + String position = titlePostionMap.get(field); + resultValuesBuilder.addRange(sheetId, position + rowCou, position + rowCou) + .addRow(fieldValue instanceof List ? GenerateUtil.getFieldValueList(fieldValue) : fieldValue); + + }); + } + } + + int rowTotal = sheet.getGridProperties().getRowCount(); + int rowNum = rowCount.get(); + if (rowNum > rowTotal) { + FsApiUtil.addRowColumns(sheetId, spreadsheetToken, "ROWS", rowTotal - rowNum, FsClientUtil.getFeishuClient()); + } + + return FsApiUtil.batchPutValues(sheetId, spreadsheetToken, resultValuesBuilder.build(), FsClientUtil.getFeishuClient()); + } +} \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/AddSheet.java b/src/main/java/cn/isliu/core/AddSheet.java new file mode 100644 index 0000000..d59c8a7 --- /dev/null +++ b/src/main/java/cn/isliu/core/AddSheet.java @@ -0,0 +1,42 @@ +package cn.isliu.core; + +import java.util.Objects; + +public class AddSheet { + + private Properties properties; + + public AddSheet() { + } + + public AddSheet(Properties properties) { + this.properties = properties; + } + + public Properties getProperties() { + return properties; + } + + public void setProperties(Properties properties) { + this.properties = properties; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + AddSheet addSheet = (AddSheet) o; + return Objects.equals(properties, addSheet.properties); + } + + @Override + public int hashCode() { + return Objects.hashCode(properties); + } + + @Override + public String toString() { + return "AddSheet{" + + "properties=" + properties + + '}'; + } +} diff --git a/src/main/java/cn/isliu/core/BaseEntity.java b/src/main/java/cn/isliu/core/BaseEntity.java new file mode 100644 index 0000000..360e2a7 --- /dev/null +++ b/src/main/java/cn/isliu/core/BaseEntity.java @@ -0,0 +1,14 @@ +package cn.isliu.core; + +public abstract class BaseEntity { + + public String uniqueId; + + public String getUniqueId() { + return uniqueId; + } + + public void setUniqueId(String uniqueId) { + this.uniqueId = uniqueId; + } +} diff --git a/src/main/java/cn/isliu/core/Cell.java b/src/main/java/cn/isliu/core/Cell.java new file mode 100644 index 0000000..655adc5 --- /dev/null +++ b/src/main/java/cn/isliu/core/Cell.java @@ -0,0 +1,72 @@ +package cn.isliu.core; + + +public class Cell { + + private int row; + private int col; + private Object value; + private Merge merge; + + public int getRow() { + return row; + } + + public void setRow(int row) { + this.row = row; + } + + public Merge getMerge() { + return merge; + } + + public void setMerge(Merge merge) { + this.merge = merge; + } + + public Object getValue() { + return value; + } + + public void setValue(Object value) { + this.value = value; + } + + public int getCol() { + return col; + } + + public void setCol(int col) { + this.col = col; + } + + @Override + public String toString() { + return "Cell{" + + "row=" + row + + ", col=" + col + + ", value=" + value + + ", merge=" + merge + + '}'; + } + + public Cell(int row, int col, Object o) { + this.row = row; + this.col = col; + this.value = o; + } + + public Cell(int row, int col, Object o, Merge merge) { + this.row = row; + this.col = col; + this.value = o; + this.merge = merge; + } + + public Cell() { + } + + public boolean isMerged() { + return merge != null; + } +} diff --git a/src/main/java/cn/isliu/core/CopySheet.java b/src/main/java/cn/isliu/core/CopySheet.java new file mode 100644 index 0000000..185c492 --- /dev/null +++ b/src/main/java/cn/isliu/core/CopySheet.java @@ -0,0 +1,43 @@ +package cn.isliu.core; + + +import java.util.Objects; + +public class CopySheet { + + private Properties properties; + + public CopySheet() { + } + + public CopySheet(Properties properties) { + this.properties = properties; + } + + public Properties getProperties() { + return properties; + } + + public void setProperties(Properties properties) { + this.properties = properties; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + CopySheet copySheet = (CopySheet) o; + return Objects.equals(properties, copySheet.properties); + } + + @Override + public int hashCode() { + return Objects.hashCode(properties); + } + + @Override + public String toString() { + return "CopySheet{" + + "properties=" + properties + + '}'; + } +} diff --git a/src/main/java/cn/isliu/core/DeleteSheet.java b/src/main/java/cn/isliu/core/DeleteSheet.java new file mode 100644 index 0000000..b29ede4 --- /dev/null +++ b/src/main/java/cn/isliu/core/DeleteSheet.java @@ -0,0 +1,53 @@ +package cn.isliu.core; + +import java.util.Objects; + +public class DeleteSheet { + + private boolean result; + private String sheetId; + + public boolean isResult() { + return result; + } + + public void setResult(boolean result) { + this.result = result; + } + + public String getSheetId() { + return sheetId; + } + + public void setSheetId(String sheetId) { + this.sheetId = sheetId; + } + + public DeleteSheet() { + } + + public DeleteSheet(boolean result, String sheetId) { + this.result = result; + this.sheetId = sheetId; + } + + @Override + public String toString() { + return "DeleteSheet{" + + "result=" + result + + ", sheetId='" + sheetId + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + DeleteSheet deleteSheet = (DeleteSheet) o; + return result == deleteSheet.result && Objects.equals(sheetId, deleteSheet.sheetId); + } + + @Override + public int hashCode() { + return Objects.hash(result, sheetId); + } +} diff --git a/src/main/java/cn/isliu/core/FsTableData.java b/src/main/java/cn/isliu/core/FsTableData.java new file mode 100644 index 0000000..40dbc8c --- /dev/null +++ b/src/main/java/cn/isliu/core/FsTableData.java @@ -0,0 +1,64 @@ +package cn.isliu.core; + +import java.util.Objects; + +public class FsTableData { + + private Integer row; + private String uniqueId; + private Object data; + + public FsTableData() { + } + + public FsTableData(Integer row, String uniqueId, Object data) { + this.row = row; + this.uniqueId = uniqueId; + this.data = data; + } + + public Integer getRow() { + return row; + } + + public void setRow(Integer row) { + this.row = row; + } + + public String getUniqueId() { + return uniqueId; + } + + public void setUniqueId(String uniqueId) { + this.uniqueId = uniqueId; + } + + public Object getData() { + return data; + } + + public void setData(Object data) { + this.data = data; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + FsTableData that = (FsTableData) o; + return Objects.equals(row, that.row) && Objects.equals(uniqueId, that.uniqueId) && Objects.equals(data, that.data); + } + + @Override + public int hashCode() { + return Objects.hash(row, uniqueId, data); + } + + @Override + public String toString() { + return "FsTableData{" + + "row=" + row + + ", uniqueId='" + uniqueId + '\'' + + ", data=" + data + + '}'; + } +} diff --git a/src/main/java/cn/isliu/core/GridProperties.java b/src/main/java/cn/isliu/core/GridProperties.java new file mode 100644 index 0000000..e93cca0 --- /dev/null +++ b/src/main/java/cn/isliu/core/GridProperties.java @@ -0,0 +1,70 @@ +package cn.isliu.core; + +import com.google.gson.annotations.SerializedName; +import java.util.Objects; + +public class GridProperties { + @SerializedName("frozen_row_count") + private int frozenRowCount; + + @SerializedName("frozen_column_count") + private int frozenColumnCount; + + @SerializedName("row_count") + private int rowCount; + + @SerializedName("column_count") + private int columnCount; + + public GridProperties() { + } + public GridProperties(int frozenRowCount, int frozenColumnCount, int rowCount, int columnCount) { + this.frozenRowCount = frozenRowCount; + this.frozenColumnCount = frozenColumnCount; + this.rowCount = rowCount; + this.columnCount = columnCount; + } + public int getFrozenRowCount() { + return frozenRowCount; + } + public int getFrozenColumnCount() { + return frozenColumnCount; + } + public int getRowCount() { + return rowCount; + } + public int getColumnCount() { + return columnCount; + } + public void setFrozenRowCount(int frozenRowCount) { + this.frozenRowCount = frozenRowCount; + } + public void setFrozenColumnCount(int frozenColumnCount) { + this.frozenColumnCount = frozenColumnCount; + } + public void setRowCount(int rowCount) { + this.rowCount = rowCount; + } + public void setColumnCount(int columnCount) { + this.columnCount = columnCount; + } + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + GridProperties gridProperties = (GridProperties) o; + return frozenRowCount == gridProperties.frozenRowCount && frozenColumnCount == gridProperties.frozenColumnCount && rowCount == gridProperties.rowCount && columnCount == gridProperties.columnCount; + } + @Override + public int hashCode() { + return Objects.hash(frozenRowCount, frozenColumnCount, rowCount, columnCount); + } + @Override + public String toString() { + return "GridProperties{" + + "frozenRowCount=" + frozenRowCount + + ", frozenColumnCount=" + frozenColumnCount + + ", rowCount=" + rowCount + + ", columnCount=" + columnCount + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/Merge.java b/src/main/java/cn/isliu/core/Merge.java new file mode 100644 index 0000000..861d565 --- /dev/null +++ b/src/main/java/cn/isliu/core/Merge.java @@ -0,0 +1,77 @@ +package cn.isliu.core; + +import com.google.gson.annotations.SerializedName; + +public class Merge { + @SerializedName("start_row_index") + private int startRowIndex; + + @SerializedName("end_row_index") + private int endRowIndex; + + @SerializedName("start_column_index") + private int startColumnIndex; + + @SerializedName("end_column_index") + private int endColumnIndex; + + public Merge(int startRowIndex, int endRowIndex, int startColumnIndex, int endColumnIndex) { + this.startRowIndex = startRowIndex; + this.endRowIndex = endRowIndex; + this.startColumnIndex = startColumnIndex; + this.endColumnIndex = endColumnIndex; + } + + public Merge() { + } + + public int getStartRowIndex() { + return startRowIndex; + } + + public void setStartRowIndex(int startRowIndex) { + this.startRowIndex = startRowIndex; + } + + public int getEndRowIndex() { + return endRowIndex; + } + + public void setEndRowIndex(int endRowIndex) { + this.endRowIndex = endRowIndex; + } + + public int getStartColumnIndex() { + return startColumnIndex; + } + + public void setStartColumnIndex(int startColumnIndex) { + this.startColumnIndex = startColumnIndex; + } + + public int getEndColumnIndex() { + return endColumnIndex; + } + + public void setEndColumnIndex(int endColumnIndex) { + this.endColumnIndex = endColumnIndex; + } + + @Override + public String toString() { + return "Merge{" + + "startRowIndex=" + startRowIndex + + ", endRowIndex=" + endRowIndex + + ", startColumnIndex=" + startColumnIndex + + ", endColumnIndex=" + endColumnIndex + + '}'; + } + + public int getRowSpan() { + return (endRowIndex - startRowIndex) + 1; + } + + public int getColSpan() { + return (endColumnIndex - startColumnIndex) + 1; + } +} \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/MergedCell.java b/src/main/java/cn/isliu/core/MergedCell.java new file mode 100644 index 0000000..9e4fbf1 --- /dev/null +++ b/src/main/java/cn/isliu/core/MergedCell.java @@ -0,0 +1,55 @@ +package cn.isliu.core; + +import java.util.Objects; + +public class MergedCell extends Cell { + + private int rowSpan; + private int colSpan; + + public MergedCell() { + super(0, 0, null); + } + + public MergedCell(int rowSpan, int colSpan) { + super(rowSpan, colSpan, null); + this.rowSpan = rowSpan; + this.colSpan = colSpan; + } + + public int getRowSpan() { + return rowSpan; + } + + public void setRowSpan(int rowSpan) { + this.rowSpan = rowSpan; + } + + public int getColSpan() { + return colSpan; + } + + public void setColSpan(int colSpan) { + this.colSpan = colSpan; + } + + @Override + public String toString() { + return "MergedCell{" + + "rowSpan=" + rowSpan + + ", colSpan=" + colSpan + + '}'; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + MergedCell that = (MergedCell) o; + return rowSpan == that.rowSpan && colSpan == that.colSpan; + } + + @Override + public int hashCode() { + return Objects.hash(rowSpan, colSpan); + } +} diff --git a/src/main/java/cn/isliu/core/Properties.java b/src/main/java/cn/isliu/core/Properties.java new file mode 100644 index 0000000..672f187 --- /dev/null +++ b/src/main/java/cn/isliu/core/Properties.java @@ -0,0 +1,65 @@ +package cn.isliu.core; + + +import java.util.Objects; + +public class Properties { + + private String sheetId; + private String title; + private int index; + + public Properties() { + } + + public Properties(String sheetId, String title, int index) { + this.sheetId = sheetId; + this.title = title; + this.index = index; + } + + public String getSheetId() { + return sheetId; + } + + public void setSheetId(String sheetId) { + this.sheetId = sheetId; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Properties that = (Properties) o; + return index == that.index && Objects.equals(sheetId, that.sheetId) && Objects.equals(title, that.title); + } + + @Override + public int hashCode() { + return Objects.hash(sheetId, title, index); + } + + @Override + public String toString() { + return "Properties{" + + "sheetId='" + sheetId + '\'' + + ", title='" + title + '\'' + + ", index=" + index + + '}'; + } +} diff --git a/src/main/java/cn/isliu/core/Reply.java b/src/main/java/cn/isliu/core/Reply.java new file mode 100644 index 0000000..66abd21 --- /dev/null +++ b/src/main/java/cn/isliu/core/Reply.java @@ -0,0 +1,64 @@ +package cn.isliu.core; + + +import java.util.Objects; + +public class Reply { + private AddSheet addSheet; + private CopySheet copySheet; + private DeleteSheet deleteSheet; + + public Reply() { + } + + public Reply(AddSheet addSheet, CopySheet copySheet, DeleteSheet deleteSheet) { + this.addSheet = addSheet; + this.copySheet = copySheet; + this.deleteSheet = deleteSheet; + } + + public AddSheet getAddSheet() { + return addSheet; + } + + public void setAddSheet(AddSheet addSheet) { + this.addSheet = addSheet; + } + + public CopySheet getCopySheet() { + return copySheet; + } + + public void setCopySheet(CopySheet copySheet) { + this.copySheet = copySheet; + } + + public DeleteSheet getDeleteSheet() { + return deleteSheet; + } + + public void setDeleteSheet(DeleteSheet deleteSheet) { + this.deleteSheet = deleteSheet; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Reply reply = (Reply) o; + return Objects.equals(addSheet, reply.addSheet) && Objects.equals(copySheet, reply.copySheet) && Objects.equals(deleteSheet, reply.deleteSheet); + } + + @Override + public int hashCode() { + return Objects.hash(addSheet, copySheet, deleteSheet); + } + + @Override + public String toString() { + return "Reply{" + + "addSheet=" + addSheet + + ", copySheet=" + copySheet + + ", deleteSheet=" + deleteSheet + + '}'; + } +} diff --git a/src/main/java/cn/isliu/core/Sheet.java b/src/main/java/cn/isliu/core/Sheet.java new file mode 100644 index 0000000..f7a0592 --- /dev/null +++ b/src/main/java/cn/isliu/core/Sheet.java @@ -0,0 +1,97 @@ +package cn.isliu.core; + +import com.google.gson.annotations.SerializedName; +import java.util.List; + +public class Sheet { + + @SerializedName("sheet_id") + private String sheetId; + + @SerializedName("title") + private String title; + + @SerializedName("index") + private int index; + + @SerializedName("hidden") + private boolean hidden; + + @SerializedName("grid_properties") + private GridProperties gridProperties; + + @SerializedName("resource_type") + private String resourceType; + + @SerializedName("merges") + private List merges; + + public Sheet() { + } + + public Sheet(String sheetId, String title, int index, boolean hidden, GridProperties gridProperties, String resourceType, List merges) { + this.sheetId = sheetId; + this.title = title; + this.index = index; + this.hidden = hidden; + this.gridProperties = gridProperties; + this.resourceType = resourceType; + this.merges = merges; + } + + public String getSheetId() { + return sheetId; + } + + public String getTitle() { + return title; + } + + public int getIndex() { + return index; + } + + public boolean isHidden() { + return hidden; + } + + public GridProperties getGridProperties() { + return gridProperties; + } + + public String getResourceType() { + return resourceType; + } + + public List getMerges() { + return merges; + } + + public void setSheetId(String sheetId) { + this.sheetId = sheetId; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setIndex(int index) { + this.index = index; + } + + public void setHidden(boolean hidden) { + this.hidden = hidden; + } + + public void setGridProperties(GridProperties gridProperties) { + this.gridProperties = gridProperties; + } + + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } + + public void setMerges(List merges) { + this.merges = merges; + } +} \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/SheetMeta.java b/src/main/java/cn/isliu/core/SheetMeta.java new file mode 100644 index 0000000..f1cd574 --- /dev/null +++ b/src/main/java/cn/isliu/core/SheetMeta.java @@ -0,0 +1,40 @@ +package cn.isliu.core; + +import com.google.gson.annotations.SerializedName; +import java.util.List; +import java.util.Objects; + +public class SheetMeta { + @SerializedName("sheets") + private List sheets; + + public List getSheets() { + return sheets; + } + + public void setSheets(List sheets) { + this.sheets = sheets; + } + + public SheetMeta() { + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + SheetMeta sheetMeta = (SheetMeta) o; + return Objects.equals(sheets, sheetMeta.sheets); + } + + @Override + public int hashCode() { + return Objects.hashCode(sheets); + } + + @Override + public String toString() { + return "SheetMeta{" + + "sheets=" + sheets + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/TableData.java b/src/main/java/cn/isliu/core/TableData.java new file mode 100644 index 0000000..b7c39c0 --- /dev/null +++ b/src/main/java/cn/isliu/core/TableData.java @@ -0,0 +1,38 @@ +package cn.isliu.core; + + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + + +public class TableData { + private List rows = new ArrayList<>(); + + public List getRows() { + return rows; + } + + public void setRows(List rows) { + this.rows = rows; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + TableData tableData = (TableData) o; + return Objects.equals(rows, tableData.rows); + } + + @Override + public int hashCode() { + return Objects.hashCode(rows); + } + + public TableData(List rows) { + this.rows = rows; + } + + public TableData() { + } +} diff --git a/src/main/java/cn/isliu/core/TableRow.java b/src/main/java/cn/isliu/core/TableRow.java new file mode 100644 index 0000000..5578385 --- /dev/null +++ b/src/main/java/cn/isliu/core/TableRow.java @@ -0,0 +1,43 @@ +package cn.isliu.core; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class TableRow { + private List cells = new ArrayList<>(); + + public List getCells() { + return cells; + } + + public void setCells(List cells) { + this.cells = cells; + } + + public TableRow(List cells) { + this.cells = cells; + } + + public TableRow() { + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + TableRow tableRow = (TableRow) o; + return Objects.equals(cells, tableRow.cells); + } + + @Override + public int hashCode() { + return Objects.hashCode(cells); + } + + @Override + public String toString() { + return "TableRow{" + + "cells=" + cells + + '}'; + } +} diff --git a/src/main/java/cn/isliu/core/ValueRange.java b/src/main/java/cn/isliu/core/ValueRange.java new file mode 100644 index 0000000..b80a44d --- /dev/null +++ b/src/main/java/cn/isliu/core/ValueRange.java @@ -0,0 +1,76 @@ +package cn.isliu.core; + +import java.util.List; +import java.util.Objects; + +public class ValueRange { + + private String majorDimension; + private String range; + private int revision; + private List> values; + + public ValueRange() { + } + + public ValueRange(String majorDimension, String range, int revision, List> values) { + this.majorDimension = majorDimension; + this.range = range; + this.revision = revision; + this.values = values; + } + + public String getMajorDimension() { + return majorDimension; + } + + public void setMajorDimension(String majorDimension) { + this.majorDimension = majorDimension; + } + + public String getRange() { + return range; + } + + public void setRange(String range) { + this.range = range; + } + + public int getRevision() { + return revision; + } + + public void setRevision(int revision) { + this.revision = revision; + } + + public List> getValues() { + return values; + } + + public void setValues(List> values) { + this.values = values; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + ValueRange that = (ValueRange) o; + return revision == that.revision && Objects.equals(majorDimension, that.majorDimension) && Objects.equals(range, that.range) && Objects.equals(values, that.values); + } + + @Override + public int hashCode() { + return Objects.hash(majorDimension, range, revision, values); + } + + @Override + public String toString() { + return "ValueRange{" + + "majorDimension='" + majorDimension + '\'' + + ", range='" + range + '\'' + + ", revision=" + revision + + ", values=" + values + + '}'; + } +} diff --git a/src/main/java/cn/isliu/core/ValuesBatch.java b/src/main/java/cn/isliu/core/ValuesBatch.java new file mode 100644 index 0000000..fa088e3 --- /dev/null +++ b/src/main/java/cn/isliu/core/ValuesBatch.java @@ -0,0 +1,76 @@ +package cn.isliu.core; + +import java.util.List; +import java.util.Objects; + +public class ValuesBatch { + private int revision; + private String spreadsheetToken; + private int totalCells; + private List valueRanges; + + public ValuesBatch() { + } + + public ValuesBatch(int revision, String spreadsheetToken, int totalCells, List valueRanges) { + this.revision = revision; + this.spreadsheetToken = spreadsheetToken; + this.totalCells = totalCells; + this.valueRanges = valueRanges; + } + + + public int getRevision() { + return revision; + } + + public void setRevision(int revision) { + this.revision = revision; + } + + public String getSpreadsheetToken() { + return spreadsheetToken; + } + + public void setSpreadsheetToken(String spreadsheetToken) { + this.spreadsheetToken = spreadsheetToken; + } + + public int getTotalCells() { + return totalCells; + } + + public void setTotalCells(int totalCells) { + this.totalCells = totalCells; + } + + public List getValueRanges() { + return valueRanges; + } + + public void setValueRanges(List valueRanges) { + this.valueRanges = valueRanges; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + ValuesBatch that = (ValuesBatch) o; + return revision == that.revision && totalCells == that.totalCells && Objects.equals(spreadsheetToken, that.spreadsheetToken) && Objects.equals(valueRanges, that.valueRanges); + } + + @Override + public int hashCode() { + return Objects.hash(revision, spreadsheetToken, totalCells, valueRanges); + } + + @Override + public String toString() { + return "ValuesBatch{" + + "revision=" + revision + + ", spreadsheetToken='" + spreadsheetToken + '\'' + + ", totalCells=" + totalCells + + ", valueRanges=" + valueRanges + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/annotation/TableProperty.java b/src/main/java/cn/isliu/core/annotation/TableProperty.java new file mode 100644 index 0000000..6739164 --- /dev/null +++ b/src/main/java/cn/isliu/core/annotation/TableProperty.java @@ -0,0 +1,28 @@ +package cn.isliu.core.annotation; + +import cn.isliu.core.converters.FieldValueProcess; +import cn.isliu.core.converters.OptionsValueProcess; +import cn.isliu.core.enums.BaseEnum; +import cn.isliu.core.enums.TypeEnum; + +import java.lang.annotation.*; + +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface TableProperty { + + String value() default ""; + + String field() default ""; + + int order() default Integer.MAX_VALUE; + + TypeEnum type() default TypeEnum.TEXT; + + Class enumClass() default BaseEnum.class; + + Class fieldFormatClass() default FieldValueProcess.class; + + Class optionsClass() default OptionsValueProcess.class; +} diff --git a/src/main/java/cn/isliu/core/client/FeishuApiClient.java b/src/main/java/cn/isliu/core/client/FeishuApiClient.java new file mode 100644 index 0000000..bca8151 --- /dev/null +++ b/src/main/java/cn/isliu/core/client/FeishuApiClient.java @@ -0,0 +1,99 @@ +package cn.isliu.core.client; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.lark.oapi.core.utils.Jsons; +import okhttp3.*; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * 飞书API客户端抽象类 提供基础的HTTP请求处理和认证逻辑 + */ +public abstract class FeishuApiClient { + protected final FeishuClient feishuClient; + protected final OkHttpClient httpClient; + protected final Gson gson; + + protected static final String BASE_URL = "https://open.feishu.cn/open-apis"; + protected static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8"); + + /** + * 构造函数 + * + * @param feishuClient 飞书客户端 + */ + public FeishuApiClient(FeishuClient feishuClient) { + this.feishuClient = feishuClient; + this.httpClient = feishuClient.getHttpClient(); + this.gson = Jsons.DEFAULT; + } + + /** + * 获取租户访问令牌 + * + * @return 访问令牌 + * @throws IOException 请求异常 + */ + protected String getTenantAccessToken() throws IOException { + Map params = new HashMap<>(); + params.put("app_id", feishuClient.getAppId()); + params.put("app_secret", feishuClient.getAppSecret()); + + RequestBody body = RequestBody.create(gson.toJson(params), JSON_MEDIA_TYPE); + + Request request = + new Request.Builder().url(BASE_URL + "/auth/v3/tenant_access_token/internal").post(body).build(); + + try (Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful() || response.body() == null) { + throw new IOException("Failed to get tenant access token: " + response); + } + + JsonObject jsonResponse = gson.fromJson(response.body().string(), JsonObject.class); + if (jsonResponse.has("tenant_access_token")) { + return jsonResponse.get("tenant_access_token").getAsString(); + } else { + throw new IOException("Invalid token response: " + jsonResponse); + } + } + } + + /** + * 构建带认证的请求 + * + * @param url 请求URL + * @param method HTTP方法 + * @param body 请求体 + * @return 请求构建器 + * @throws IOException 认证异常 + */ + protected Request.Builder createAuthenticatedRequest(String url, String method, RequestBody body) + throws IOException { + String token = getTenantAccessToken(); + return new Request.Builder().url(url).header("Authorization", "Bearer " + token) + .header("Content-Type", "application/json; charset=utf-8").method(method, body); + } + + /** + * 执行请求并处理响应 + * + * @param request 请求对象 + * @param responseClass 响应类型 + * @param 响应类型 + * @return 响应对象 + * @throws IOException 请求异常 + */ + protected T executeRequest(Request request, Class responseClass) throws IOException { + try (Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful() || response.body() == null) { + throw new IOException("Request failed: " + response); + } + + String responseBody = response.body().string(); + return gson.fromJson(responseBody, responseClass); + } + } +} \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/client/FeishuClient.java b/src/main/java/cn/isliu/core/client/FeishuClient.java new file mode 100644 index 0000000..b833450 --- /dev/null +++ b/src/main/java/cn/isliu/core/client/FeishuClient.java @@ -0,0 +1,266 @@ +package cn.isliu.core.client; + +import cn.isliu.core.service.*; +import com.lark.oapi.Client; +import com.lark.oapi.core.enums.AppType; +import com.lark.oapi.service.drive.DriveService; +import com.lark.oapi.service.sheets.SheetsService; +import okhttp3.ConnectionPool; +import okhttp3.OkHttpClient; + +import java.util.concurrent.TimeUnit; + +/** + * 飞书扩展客户端 封装官方SDK客户端并提供额外API支持 + */ +public class FeishuClient { + private final Client officialClient; + private final OkHttpClient httpClient; + private final String appId; + private final String appSecret; + + // 自定义服务,处理官方SDK未覆盖的API + private volatile CustomSheetService customSheetService; + private volatile CustomDimensionService customDimensionService; + private volatile CustomCellService customCellService; + private volatile CustomValueService customValueService; + private volatile CustomDataValidationService customDataValidationService; + private volatile CustomProtectedDimensionService customProtectedDimensionService; + + private FeishuClient(String appId, String appSecret, Client officialClient, OkHttpClient httpClient) { + this.appId = appId; + this.appSecret = appSecret; + this.officialClient = officialClient; + this.httpClient = httpClient; + } + + /** + * 创建客户端构建器 + * + * @param appId 应用ID + * @param appSecret 应用密钥 + * @return 构建器 + */ + public static Builder newBuilder(String appId, String appSecret) { + return new Builder(appId, appSecret); + } + + /** + * 获取官方表格服务 + * + * @return 官方表格服务 + */ + public SheetsService sheets() { + return officialClient.sheets(); + } + + /** + * 获取官方驱动服务 + * + * @return 官方驱动服务 + */ + public DriveService drive() { + return officialClient.drive(); + } + + /** + * 获取扩展表格服务 + * + * @return 扩展表格服务 + */ + public CustomSheetService customSheets() { + if (customSheetService == null) { + synchronized (this) { + if (customSheetService == null) { + customSheetService = new CustomSheetService(this); + } + } + } + return customSheetService; + } + + /** + * 获取扩展行列服务 + * + * @return 扩展行列服务 + */ + public CustomDimensionService customDimensions() { + if (customDimensionService == null) { + synchronized (this) { + if (customDimensionService == null) { + customDimensionService = new CustomDimensionService(this); + } + } + } + return customDimensionService; + } + + /** + * 获取扩展单元格服务 + * + * @return 扩展单元格服务 + */ + public CustomCellService customCells() { + if (customCellService == null) { + synchronized (this) { + if (customCellService == null) { + customCellService = new CustomCellService(this); + } + } + } + return customCellService; + } + + /** + * 获取扩展数据值服务 + * + * @return 扩展数据值服务 + */ + public CustomValueService customValues() { + if (customValueService == null) { + synchronized (this) { + if (customValueService == null) { + customValueService = new CustomValueService(this); + } + } + } + return customValueService; + } + + /** + * 获取自定义数据验证服务 + * + * @return 自定义数据验证服务 + */ + public CustomDataValidationService customDataValidations() { + if (customDataValidationService == null) { + synchronized (this) { + if (customDataValidationService == null) { + customDataValidationService = new CustomDataValidationService(this); + } + } + } + return customDataValidationService; + } + + /** + * 获取扩展保护范围服务 + * + * @return 扩展保护范围服务 + */ + public CustomProtectedDimensionService customProtectedDimensions() { + if (customProtectedDimensionService == null) { + synchronized (this) { + if (customProtectedDimensionService == null) { + customProtectedDimensionService = new CustomProtectedDimensionService(this); + } + } + } + return customProtectedDimensionService; + } + + /** + * 获取官方客户端 + * + * @return 官方Client实例 + */ + public Client getOfficialClient() { + return officialClient; + } + + /** + * 获取HTTP客户端 + * + * @return OkHttp客户端实例 + */ + OkHttpClient getHttpClient() { + return httpClient; + } + + /** + * 获取应用ID + * + * @return 应用ID + */ + public String getAppId() { + return appId; + } + + /** + * 获取应用密钥 + * + * @return 应用密钥 + */ + public String getAppSecret() { + return appSecret; + } + + /** + * FeishuClient构建器 + */ + public static class Builder { + private final String appId; + private final String appSecret; + private OkHttpClient.Builder httpClientBuilder; + private AppType appType = AppType.SELF_BUILT; + private boolean logReqAtDebug = false; + + private Builder(String appId, String appSecret) { + this.appId = appId; + this.appSecret = appSecret; + // 默认OkHttp配置 + this.httpClientBuilder = + new OkHttpClient.Builder().connectTimeout(10, TimeUnit.MINUTES).readTimeout(10, TimeUnit.MINUTES) + .writeTimeout(10, TimeUnit.MINUTES).connectionPool(new ConnectionPool(5, 5, TimeUnit.MINUTES)); + } + + /** + * 配置HTTP客户端 + * + * @param builder OkHttp客户端构建器 + * @return 当前构建器 + */ + public Builder httpClient(OkHttpClient.Builder builder) { + this.httpClientBuilder = builder; + return this; + } + + /** + * 设置应用类型 + * + * @param appType 应用类型 + * @return 当前构建器 + */ + public Builder appType(AppType appType) { + this.appType = appType; + return this; + } + + /** + * 是否在debug级别打印请求 + * + * @param logReqAtDebug 是否打印 + * @return 当前构建器 + */ + public Builder logReqAtDebug(boolean logReqAtDebug) { + this.logReqAtDebug = logReqAtDebug; + return this; + } + + /** + * 构建FeishuClient实例 + * + * @return FeishuClient实例 + */ + public FeishuClient build() { + // 构建官方Client + Client officialClient = + Client.newBuilder(appId, appSecret).appType(appType).logReqAtDebug(logReqAtDebug).build(); + + // 构建OkHttpClient + OkHttpClient httpClient = httpClientBuilder.build(); + + return new FeishuClient(appId, appSecret, officialClient, httpClient); + } + } +} \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/config/FsConfig.java b/src/main/java/cn/isliu/core/config/FsConfig.java new file mode 100644 index 0000000..823d83e --- /dev/null +++ b/src/main/java/cn/isliu/core/config/FsConfig.java @@ -0,0 +1,56 @@ +package cn.isliu.core.config; + +import cn.isliu.core.client.FeishuClient; +import cn.isliu.core.utils.FsClientUtil; + +public class FsConfig { + + + public static int headLine = 1; + + public static int titleLine = 1; + + public static boolean isCover = false; + public static boolean CELL_TEXT = false; + public static String FORE_COLOR = "#000000"; + public static String BACK_COLOR = "#d5d5d5"; + + public static void initConfig(String appId, String appSecret) { + FsClientUtil.initFeishuClient(appId, appSecret); + } + + public static void initConfig(int headLine, int titleLine, String appId, String appSecret) { + FsConfig.headLine = headLine; + FsConfig.titleLine = titleLine; + FsClientUtil.initFeishuClient(appId, appSecret); + } + + public static void initConfig(int headLine, FeishuClient client) { + FsConfig.headLine = headLine; + FsClientUtil.client = client; + } + + public static void initConfig(FeishuClient client) { + FsClientUtil.client = client; + } + + public static int getHeadLine() { + return headLine; + } + + public static int getTitleLine() { + return titleLine; + } + + public static FeishuClient getFeishuClient() { + return FsClientUtil.client; + } + + public static void setHeadLine(int headLine) { + FsConfig.headLine = headLine; + } + + public static void setTitleLine(int titleLine) { + FsConfig.titleLine = titleLine; + } +} diff --git a/src/main/java/cn/isliu/core/converters/FieldValueProcess.java b/src/main/java/cn/isliu/core/converters/FieldValueProcess.java new file mode 100644 index 0000000..2eb441c --- /dev/null +++ b/src/main/java/cn/isliu/core/converters/FieldValueProcess.java @@ -0,0 +1,11 @@ +package cn.isliu.core.converters; + +public interface FieldValueProcess { + + T process(Object value); + + /** + * 反向处理,将枚举值转换为原始值 + */ + T reverseProcess(Object value); +} diff --git a/src/main/java/cn/isliu/core/converters/FileUrlProcess.java b/src/main/java/cn/isliu/core/converters/FileUrlProcess.java new file mode 100644 index 0000000..35aee3f --- /dev/null +++ b/src/main/java/cn/isliu/core/converters/FileUrlProcess.java @@ -0,0 +1,128 @@ +package cn.isliu.core.converters; + +import cn.isliu.core.utils.FsApiUtil; +import cn.isliu.core.utils.FsClientUtil; +import cn.isliu.core.utils.FileUtil; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class FileUrlProcess implements FieldValueProcess { + + private static final Logger log = Logger.getLogger(FileUrlProcess.class.getName()); + + @Override + public String process(Object value) { + if (value instanceof String) { + return value.toString(); + } + + List fileUrls = new ArrayList<>(); + if (value instanceof JsonArray) { + JsonArray arr = (JsonArray) value; + for (int i = 0; i < arr.size(); i++) { + JsonElement jsonElement = arr.get(i); + if (jsonElement.isJsonObject()) { + JsonObject jsonObject = jsonElement.getAsJsonObject(); + String url = getUrlByTextFile(jsonObject); + fileUrls.add(url); + } + } + } else if (value instanceof JsonObject) { + JsonObject jsb = (JsonObject) value; + String url = getUrlByTextFile(jsb); + fileUrls.add(url); + } + return String.join(",", fileUrls); + } + + @Override + public String reverseProcess(Object value) { + // 简单实现,可以根据需要进行更复杂的反向处理 + if (value == null) { + return null; + } + return value.toString(); + } + + private synchronized String getUrlByTextFile(JsonObject jsb) { + String url = ""; + String cellType = jsb.get("type").getAsString(); + + switch (cellType) { + case "url": + String link = jsb.get("link").getAsString(); + if (link == null) { + url = jsb.get("text").getAsString(); + } else { + url = link; + } + break; + case "embed-image": + url = getImageOssUrl(jsb); + break; + case "attachment": + url = getAttachmentOssUrl(jsb); + break; + } + return url; + } + + public static String getImageOssUrl(JsonObject jsb) { + String url = ""; + String fileToken = jsb.get("fileToken").getAsString(); + + String fileUuid = UUID.randomUUID().toString(); + String filePath = FileUtil.getRootPath() + File.separator + fileUuid + ".png"; + + boolean isSuccess = true; + try { + FsApiUtil.downloadMaterial(fileToken, filePath , FsClientUtil.getFeishuClient(), null); + url = filePath; + } catch (Exception e) { + log.log(Level.WARNING,"【飞书表格】 根据文件FileToken下载失败!fileToken: {0}, e: {1}", new Object[]{fileToken, e.getMessage()}); + isSuccess = false; + } + + if (!isSuccess) { + String tmpUrl = FsApiUtil.downloadTmpMaterialUrl(fileToken, FsClientUtil.getFeishuClient()); + // 根据临时下载地址下载 + FileUtil.downloadFile(tmpUrl, filePath); + } + + log.info("【飞书表格】 文件上传-飞书图片上传成功!fileToken: " + fileToken + ", filePath: " + filePath); + return url; + } + + public String getAttachmentOssUrl(JsonObject jsb) { + String url = ""; + String token = jsb.get("fileToken").getAsString(); + String fileName = jsb.get("text").getAsString(); + + String fileUuid = UUID.randomUUID().toString(); + String path = FileUtil.getRootPath() + File.separator + fileUuid + fileName; + + boolean isSuccess = true; + try { + FsApiUtil.downloadMaterial(token, path , FsClientUtil.getFeishuClient(), null); + url = path; + } catch (Exception e) { + log.log(Level.WARNING, "【飞书表格】 附件-根据文件FileToken下载失败!fileToken: {0}, e: {1}", new Object[]{token, e.getMessage()}); + isSuccess = false; + } + + if (!isSuccess) { + String tmpUrl = FsApiUtil.downloadTmpMaterialUrl(token, FsClientUtil.getFeishuClient()); + FileUtil.downloadFile(tmpUrl, path); + } + + log.info("【飞书表格】 文件上传-附件上传成功!fileToken: " + token + ", filePath: " + path); + return url; + } +} diff --git a/src/main/java/cn/isliu/core/converters/OptionsValueProcess.java b/src/main/java/cn/isliu/core/converters/OptionsValueProcess.java new file mode 100644 index 0000000..50e8afa --- /dev/null +++ b/src/main/java/cn/isliu/core/converters/OptionsValueProcess.java @@ -0,0 +1,6 @@ +package cn.isliu.core.converters; + +public interface OptionsValueProcess { + + T process(); +} diff --git a/src/main/java/cn/isliu/core/enums/BaseEnum.java b/src/main/java/cn/isliu/core/enums/BaseEnum.java new file mode 100644 index 0000000..e8a3561 --- /dev/null +++ b/src/main/java/cn/isliu/core/enums/BaseEnum.java @@ -0,0 +1,53 @@ +package cn.isliu.core.enums; + +import java.util.Arrays; + +public interface BaseEnum { + + /** + * 获取枚举代码 + */ + String getCode(); + + /** + * 获取枚举描述 + */ + String getDesc(); + + /** + * 根据描述获取枚举实例 + * + * @param enumClass 枚举类 + * @param desc 描述 + * @param 枚举类型 + * @return 枚举实例 + */ + static T getByDesc(Class enumClass, Object desc) { + if (desc == null) { + return null; + } + + return Arrays.stream(enumClass.getEnumConstants()) + .filter(e -> e.getDesc().equals(desc.toString())) + .findFirst() + .orElse(null); + } + + /** + * 根据代码获取枚举实例 + * + * @param enumClass 枚举类 + * @param code 代码 + * @param 枚举类型 + * @return 枚举实例 + */ + static T getByCode(Class enumClass, Object code) { + if (code == null) { + return null; + } + return Arrays.stream(enumClass.getEnumConstants()) + .filter(e -> e.getCode().equals(code.toString())) + .findFirst() + .orElse(null); + } +} \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/enums/TypeEnum.java b/src/main/java/cn/isliu/core/enums/TypeEnum.java new file mode 100644 index 0000000..066a62a --- /dev/null +++ b/src/main/java/cn/isliu/core/enums/TypeEnum.java @@ -0,0 +1,41 @@ +package cn.isliu.core.enums; + + +public enum TypeEnum { + + SINGLE_SELECT("SINGLE_SELECT", "单选"), + MULTI_SELECT("MULTI_SELECT", "多选"), + TEXT("TEXT", "文本"), + NUMBER("NUMBER", "数字"), + DATE("DATE", "日期"), + TEXT_FILE("TEXT_FILE", "文本文件"), + MULTI_TEXT("MULTI_TEXT", "多个文本(逗号分割)"), + TEXT_URL("TEXT_URL", "文本链接") + + ; + + private final String code; + private final String desc; + + public String getCode() { + return code; + } + + public String getDesc() { + return desc; + } + + TypeEnum(String code, String desc) { + this.code = code; + this.desc = desc; + } + + public static TypeEnum getByCode(String code) { + for (TypeEnum value : values()) { + if (value.getCode().equals(code)) { + return value; + } + } + return null; + } +} diff --git a/src/main/java/cn/isliu/core/exception/FsHelperException.java b/src/main/java/cn/isliu/core/exception/FsHelperException.java new file mode 100644 index 0000000..e78886d --- /dev/null +++ b/src/main/java/cn/isliu/core/exception/FsHelperException.java @@ -0,0 +1,12 @@ +package cn.isliu.core.exception; + +public class FsHelperException extends RuntimeException { + + public FsHelperException(String message) { + super(message); + } + + public FsHelperException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/cn/isliu/core/pojo/ApiResponse.java b/src/main/java/cn/isliu/core/pojo/ApiResponse.java new file mode 100644 index 0000000..3c1daa0 --- /dev/null +++ b/src/main/java/cn/isliu/core/pojo/ApiResponse.java @@ -0,0 +1,80 @@ +package cn.isliu.core.pojo; + +/** + * API响应基类 + * + * @param 响应数据类型 + */ +public class ApiResponse { + + private int code; + private String msg; + private T data; + + /** + * 无参构造函数 + */ + public ApiResponse() {} + + /** + * 构造函数 + * + * @param code 响应码 + * @param msg 响应消息 + * @param data 响应数据 + */ + public ApiResponse(int code, String msg, T data) { + this.code = code; + this.msg = msg; + this.data = data; + } + + /** + * 获取响应码 + * + * @return 响应码 + */ + public int getCode() { + return code; + } + + /** + * 获取响应消息 + * + * @return 响应消息 + */ + public String getMsg() { + return msg; + } + + /** + * 获取响应数据 + * + * @return 响应数据 + */ + public T getData() { + return data; + } + + public void setCode(int code) { + this.code = code; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + public void setData(T data) { + this.data = data; + } + + /** + * 判断请求是否成功 + * + * @return true表示成功,false表示失败 + */ + public boolean success() { + return code == 0; + } + +} diff --git a/src/main/java/cn/isliu/core/pojo/FieldProperty.java b/src/main/java/cn/isliu/core/pojo/FieldProperty.java new file mode 100644 index 0000000..a687e10 --- /dev/null +++ b/src/main/java/cn/isliu/core/pojo/FieldProperty.java @@ -0,0 +1,65 @@ +package cn.isliu.core.pojo; + +import cn.isliu.core.annotation.TableProperty; +import cn.isliu.core.converters.FieldValueProcess; +import cn.isliu.core.enums.BaseEnum; +import cn.isliu.core.enums.TypeEnum; + +public class FieldProperty { + + private String field; + private TableProperty tableProperty; + + public FieldProperty() { + } + + public FieldProperty(String field, TableProperty tableProperty) { + this.field = field; + this.tableProperty = tableProperty; + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + + public TableProperty getTableProperty() { + return tableProperty; + } + + public void setTableProperty(TableProperty tableProperty) { + this.tableProperty = tableProperty; + } + + public String getFieldField() { + return tableProperty.field(); + } + + public String getFieldName() { + return tableProperty.value(); + } + + public TypeEnum getFieldType() { + return tableProperty.type(); + } + + public Class getFieldFormat() { + return tableProperty.fieldFormatClass(); + } + + public Class getFieldEnum() { + return tableProperty.enumClass(); + } + + @Override + public String toString() { + return "FieldProperty{" + + "field='" + field + '\'' + + ", tableProperty=" + tableProperty + + '}'; + } + +} diff --git a/src/main/java/cn/isliu/core/service/CustomCellService.java b/src/main/java/cn/isliu/core/service/CustomCellService.java new file mode 100644 index 0000000..386f938 --- /dev/null +++ b/src/main/java/cn/isliu/core/service/CustomCellService.java @@ -0,0 +1,2164 @@ +package cn.isliu.core.service; + + +import cn.isliu.core.client.FeishuApiClient; +import cn.isliu.core.client.FeishuClient; +import cn.isliu.core.pojo.ApiResponse; +import okhttp3.Request; +import okhttp3.RequestBody; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * 自定义单元格服务 提供官方SDK未覆盖的单元格操作API + */ +public class CustomCellService extends FeishuApiClient { + + /** + * 构造函数 + * + * @param feishuClient 飞书客户端 + */ + public CustomCellService(FeishuClient feishuClient) { + super(feishuClient); + } + + /** + * 批量操作单元格 支持合并单元格等操作 支持处理多个请求,如果有请求失败则中断后续请求 + * + * @param spreadsheetToken 电子表格Token + * @param request 批量操作请求 + * @return 批量操作响应 + * @throws IOException 请求异常 + */ + public ApiResponse cellsBatchUpdate(String spreadsheetToken, CellBatchUpdateRequest request) + throws IOException { + List requests = request.getRequests(); + ApiResponse response = null; + + // 如果没有请求,返回空响应 + if (requests == null || requests.isEmpty()) { + ApiResponse emptyResponse = new ApiResponse(); + emptyResponse.setCode(400); + emptyResponse.setMsg("No cell operations found"); + return emptyResponse; + } + + // 依次处理每个请求 + for (CellRequest cellRequest : requests) { + // 处理合并单元格请求 + if (cellRequest.getMergeCells() != null) { + String url = BASE_URL + "/sheets/v2/spreadsheets/" + spreadsheetToken + "/merge_cells"; + + String params; + + String type = cellRequest.getMergeCells().getType(); + if (type != null && !type.isEmpty() && "JSON_STR".equals(type)) { + params = cellRequest.getMergeCells().getParams(); + } else { + // 获取合并单元格范围 + String range = cellRequest.getMergeCells().getRange(); + if (range == null) { + ApiResponse errorResponse = new ApiResponse(); + errorResponse.setCode(400); + errorResponse.setMsg("Invalid cell range"); + return errorResponse; + } + params = gson.toJson(new MergeCellsRequestBody(range, cellRequest.getMergeCells().getMergeType())); + } + + // 构建合并单元格请求体 + RequestBody body = RequestBody.create(params, JSON_MEDIA_TYPE); + + Request httpRequest = createAuthenticatedRequest(url, "POST", body).build(); + response = executeRequest(httpRequest, ApiResponse.class); + + // 如果请求失败,中断后续请求 + if (!response.success()) { + return response; + } + } + // 处理拆分单元格请求 + else if (cellRequest.getUnmergeCells() != null) { + String url = BASE_URL + "/sheets/v2/spreadsheets/" + spreadsheetToken + "/unmerge_cells"; + + // 获取拆分单元格范围 + String range = cellRequest.getUnmergeCells().getRange(); + if (range == null) { + ApiResponse errorResponse = new ApiResponse(); + errorResponse.setCode(400); + errorResponse.setMsg("Invalid cell range"); + return errorResponse; + } + + // 构建拆分单元格请求体 + RequestBody body = RequestBody.create(gson.toJson(new UnmergeCellsRequestBody(range)), JSON_MEDIA_TYPE); + + Request httpRequest = createAuthenticatedRequest(url, "POST", body).build(); + response = executeRequest(httpRequest, ApiResponse.class); + + // 如果请求失败,中断后续请求 + if (!response.success()) { + return response; + } + } + // 处理设置单元格样式请求 + else if (cellRequest.getStyleCells() != null) { + String url = BASE_URL + "/sheets/v2/spreadsheets/" + spreadsheetToken + "/style"; + + // 获取单元格范围 + String range = cellRequest.getStyleCells().getRange(); + if (range == null) { + ApiResponse errorResponse = new ApiResponse(); + errorResponse.setCode(400); + errorResponse.setMsg("Invalid cell range"); + return errorResponse; + } + + // 构建设置样式请求体 + RequestBody body = RequestBody.create( + gson.toJson(new StyleCellsRequestBody(range, cellRequest.getStyleCells().getStyle())), + JSON_MEDIA_TYPE); + + Request httpRequest = createAuthenticatedRequest(url, "PUT", body).build(); + response = executeRequest(httpRequest, ApiResponse.class); + + // 如果请求失败,中断后续请求 + if (!response.success()) { + return response; + } + } + // 处理批量设置单元格样式请求 + else if (cellRequest.getStyleCellsBatch() != null) { + String url = BASE_URL + "/sheets/v2/spreadsheets/" + spreadsheetToken + "/styles_batch_update"; + + String type = cellRequest.getStyleCellsBatch().getType(); + String params = ""; + if (type != null && !type.isEmpty() && "JSON_STR".equals(type)) { + params = cellRequest.getStyleCellsBatch().getParams(); + } else { + // 获取单元格范围和样式 + List ranges = cellRequest.getStyleCellsBatch().getRanges(); + Style style = cellRequest.getStyleCellsBatch().getStyle(); + + if (ranges == null || ranges.isEmpty()) { + ApiResponse errorResponse = new ApiResponse(); + errorResponse.setCode(400); + errorResponse.setMsg("Invalid cell ranges"); + return errorResponse; + } + + // 构建批量设置样式请求体 + StyleBatchUpdateRequest styleBatchRequest = new StyleBatchUpdateRequest(); + StyleBatchData styleBatchData = new StyleBatchData(); + styleBatchData.setRanges(ranges); + styleBatchData.setStyle(style); + styleBatchRequest.getData().add(styleBatchData); + params = gson.toJson(styleBatchRequest); + } + + RequestBody body = RequestBody.create(params, JSON_MEDIA_TYPE); + Request httpRequest = createAuthenticatedRequest(url, "PUT", body).build(); + response = executeRequest(httpRequest, ApiResponse.class); + + // 如果请求失败,中断后续请求 + if (!response.success()) { + return response; + } + } + // 这里可以添加其他单元格操作类型 + } + + // 如果所有请求都成功处理,返回最后一个成功的响应 + // 如果没有处理任何请求(没有有效的操作类型),返回错误响应 + if (response == null) { + ApiResponse errorResponse = new ApiResponse(); + errorResponse.setCode(400); + errorResponse.setMsg("No valid cell operation found"); + return errorResponse; + } + + return response; + } + + /** + * 批量操作单元格请求 + */ + public static class CellBatchUpdateRequest { + private List requests; + + public CellBatchUpdateRequest() { + this.requests = new ArrayList<>(); + } + + public List getRequests() { + return requests; + } + + public void setRequests(List requests) { + this.requests = requests; + } + + /** + * 创建批量操作单元格请求的构建器 + * + * @return 批量操作单元格请求的构建器 + */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * 批量操作单元格请求的构建器 + */ + public static class Builder { + private final CellBatchUpdateRequest request; + + public Builder() { + request = new CellBatchUpdateRequest(); + } + + /** + * 添加一个单元格操作请求 + * + * @param cellRequest 单元格操作请求 + * @return 当前构建器 + */ + public Builder addRequest(CellRequest cellRequest) { + request.requests.add(cellRequest); + return this; + } + + /** + * 构建批量操作单元格请求 + * + * @return 批量操作单元格请求 + */ + public CellBatchUpdateRequest build() { + return request; + } + } + } + + /** + * 单元格操作请求 + */ + public static class CellRequest { + private MergeCellsRequest mergeCells; + private UnmergeCellsRequest unmergeCells; + private StyleCellsRequest styleCells; + private StyleCellsBatchRequest styleCellsBatch; + + /** + * 获取合并单元格请求 + * + * @return 合并单元格请求 + */ + public MergeCellsRequest getMergeCells() { + return mergeCells; + } + + /** + * 设置合并单元格请求 + * + * @param mergeCells 合并单元格请求 + */ + public void setMergeCells(MergeCellsRequest mergeCells) { + this.mergeCells = mergeCells; + } + + /** + * 获取拆分单元格请求 + * + * @return 拆分单元格请求 + */ + public UnmergeCellsRequest getUnmergeCells() { + return unmergeCells; + } + + /** + * 设置拆分单元格请求 + * + * @param unmergeCells 拆分单元格请求 + */ + public void setUnmergeCells(UnmergeCellsRequest unmergeCells) { + this.unmergeCells = unmergeCells; + } + + /** + * 获取设置单元格样式请求 + * + * @return 设置单元格样式请求 + */ + public StyleCellsRequest getStyleCells() { + return styleCells; + } + + /** + * 设置单元格样式请求 + * + * @param styleCells 设置单元格样式请求 + */ + public void setStyleCells(StyleCellsRequest styleCells) { + this.styleCells = styleCells; + } + + /** + * 获取批量设置单元格样式请求 + * + * @return 批量设置单元格样式请求 + */ + public StyleCellsBatchRequest getStyleCellsBatch() { + return styleCellsBatch; + } + + /** + * 设置批量设置单元格样式请求 + * + * @param styleCellsBatch 批量设置单元格样式请求 + */ + public void setStyleCellsBatch(StyleCellsBatchRequest styleCellsBatch) { + this.styleCellsBatch = styleCellsBatch; + } + + /** + * 创建合并单元格的请求构建器 用于合并指定范围的单元格 + * + * @return 合并单元格的构建器 + */ + public static MergeCellsBuilder mergeCells() { + return new MergeCellsBuilder(); + } + + /** + * 创建拆分单元格的请求构建器 用于拆分指定范围的单元格 + * + * @return 拆分单元格的构建器 + */ + public static UnmergeCellsBuilder unmergeCells() { + return new UnmergeCellsBuilder(); + } + + /** + * 创建设置单元格样式的请求构建器 用于设置指定范围单元格的样式 + * + * @return 设置单元格样式的构建器 + */ + public static StyleCellsBuilder styleCells() { + return new StyleCellsBuilder(); + } + + /** + * 创建批量设置单元格样式的请求构建器 用于一次设置多个区域单元格的样式 + * + * @return 批量设置单元格样式的构建器 + */ + public static StyleCellsBatchBuilder styleCellsBatch() { + return new StyleCellsBatchBuilder(); + } + + /** + * 合并单元格的构建器 用于构建合并单元格的请求 + */ + public static class MergeCellsBuilder { + private final CellRequest request; + private final MergeCellsRequest mergeCells; + + public MergeCellsBuilder() { + request = new CellRequest(); + mergeCells = new MergeCellsRequest(); + request.setMergeCells(mergeCells); + } + + public MergeCellsBuilder setReqType(String reqType) { + mergeCells.setType(reqType); + return this; + } + + public MergeCellsBuilder setReqParams(String reqParams) { + mergeCells.setParams(reqParams); + return this; + } + + /** + * 设置要合并的单元格所在的工作表ID + * + * @param sheetId 工作表ID + * @return 当前构建器 + */ + public MergeCellsBuilder sheetId(String sheetId) { + mergeCells.sheetId = sheetId; + return this; + } + + /** + * 设置要合并的单元格范围的开始位置 + * + * @param startPosition 开始位置,如A1 + * @return 当前构建器 + */ + public MergeCellsBuilder startPosition(String startPosition) { + mergeCells.startPosition = startPosition; + return this; + } + + /** + * 设置要合并的单元格范围的结束位置 + * + * @param endPosition 结束位置,如B2 + * @return 当前构建器 + */ + public MergeCellsBuilder endPosition(String endPosition) { + mergeCells.endPosition = endPosition; + return this; + } + + /** + * 设置合并方式 + * + * @param mergeType 合并方式,可选值:MERGE_ALL(合并所有单元格)、MERGE_ROWS(按行合并)、MERGE_COLUMNS(按列合并) + * @return 当前构建器 + */ + public MergeCellsBuilder mergeType(String mergeType) { + mergeCells.mergeType = mergeType; + return this; + } + + /** + * 构建合并单元格请求 + * + * @return 单元格操作请求 + */ + public CellRequest build() { + return request; + } + } + + /** + * 拆分单元格的构建器 用于构建拆分单元格的请求 + */ + public static class UnmergeCellsBuilder { + private final CellRequest request; + private final UnmergeCellsRequest unmergeCells; + + public UnmergeCellsBuilder() { + request = new CellRequest(); + unmergeCells = new UnmergeCellsRequest(); + request.setUnmergeCells(unmergeCells); + } + + /** + * 设置要拆分的单元格所在的工作表ID + * + * @param sheetId 工作表ID + * @return 当前构建器 + */ + public UnmergeCellsBuilder sheetId(String sheetId) { + unmergeCells.sheetId = sheetId; + return this; + } + + /** + * 设置要拆分的单元格范围的开始位置 + * + * @param startPosition 开始位置,如A1 + * @return 当前构建器 + */ + public UnmergeCellsBuilder startPosition(String startPosition) { + unmergeCells.startPosition = startPosition; + return this; + } + + /** + * 设置要拆分的单元格范围的结束位置 + * + * @param endPosition 结束位置,如B2 + * @return 当前构建器 + */ + public UnmergeCellsBuilder endPosition(String endPosition) { + unmergeCells.endPosition = endPosition; + return this; + } + + /** + * 构建拆分单元格请求 + * + * @return 单元格操作请求 + */ + public CellRequest build() { + return request; + } + } + } + + /** + * 合并单元格请求 + */ + public static class MergeCellsRequest { + /** + * 工作表ID + */ + private String sheetId; + + /** + * 单元格范围的开始位置 + */ + private String startPosition; + + /** + * 单元格范围的结束位置 + */ + private String endPosition; + + /** + * 兼容旧版接口的完整range + */ + private String legacyRange; + + /** + * 合并方式 可选值: MERGE_ALL:合并所有单元格 MERGE_ROWS:按行合并 MERGE_COLUMNS:按列合并 + */ + private String mergeType; + + private String type; + private String params; + + /** + * 获取要合并的单元格范围 + * + * @return 单元格范围,格式为 !<开始位置>:<结束位置> + */ + public String getRange() { + if (legacyRange != null && !legacyRange.isEmpty()) { + return legacyRange; + } + + if (sheetId != null && startPosition != null && endPosition != null) { + return sheetId + "!" + startPosition + ":" + endPosition; + } + + return null; + } + + /** + * 获取工作表ID + * + * @return 工作表ID + */ + public String getSheetId() { + return sheetId; + } + + /** + * 设置工作表ID + * + * @param sheetId 工作表ID + */ + public void setSheetId(String sheetId) { + this.sheetId = sheetId; + } + + /** + * 获取单元格范围的开始位置 + * + * @return 开始位置 + */ + public String getStartPosition() { + return startPosition; + } + + /** + * 设置单元格范围的开始位置 + * + * @param startPosition 开始位置,如A1 + */ + public void setStartPosition(String startPosition) { + this.startPosition = startPosition; + } + + /** + * 获取单元格范围的结束位置 + * + * @return 结束位置 + */ + public String getEndPosition() { + return endPosition; + } + + /** + * 设置单元格范围的结束位置 + * + * @param endPosition 结束位置,如B2 + */ + public void setEndPosition(String endPosition) { + this.endPosition = endPosition; + } + + /** + * 获取合并方式 + * + * @return 合并方式 + */ + public String getMergeType() { + return mergeType; + } + + /** + * 设置合并方式 + * + * @param mergeType 合并方式,可选值:MERGE_ALL、MERGE_ROWS、MERGE_COLUMNS + */ + public void setMergeType(String mergeType) { + this.mergeType = mergeType; + } + + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getParams() { + return params; + } + + public void setParams(String params) { + this.params = params; + } + } + + /** + * 拆分单元格请求 + */ + public static class UnmergeCellsRequest { + /** + * 工作表ID + */ + private String sheetId; + + /** + * 单元格范围的开始位置 + */ + private String startPosition; + + /** + * 单元格范围的结束位置 + */ + private String endPosition; + + /** + * 兼容旧版接口的完整range + */ + private String legacyRange; + + /** + * 获取要拆分的单元格范围 + * + * @return 单元格范围,格式为 !<开始位置>:<结束位置> + */ + public String getRange() { + if (legacyRange != null && !legacyRange.isEmpty()) { + return legacyRange; + } + + if (sheetId != null && startPosition != null && endPosition != null) { + return sheetId + "!" + startPosition + ":" + endPosition; + } + + return null; + } + + /** + * 获取工作表ID + * + * @return 工作表ID + */ + public String getSheetId() { + return sheetId; + } + + /** + * 设置工作表ID + * + * @param sheetId 工作表ID + */ + public void setSheetId(String sheetId) { + this.sheetId = sheetId; + } + + /** + * 获取单元格范围的开始位置 + * + * @return 开始位置 + */ + public String getStartPosition() { + return startPosition; + } + + /** + * 设置单元格范围的开始位置 + * + * @param startPosition 开始位置,如A1 + */ + public void setStartPosition(String startPosition) { + this.startPosition = startPosition; + } + + /** + * 获取单元格范围的结束位置 + * + * @return 结束位置 + */ + public String getEndPosition() { + return endPosition; + } + + /** + * 设置单元格范围的结束位置 + * + * @param endPosition 结束位置,如B2 + */ + public void setEndPosition(String endPosition) { + this.endPosition = endPosition; + } + } + + /** + * 设置单元格样式请求 + */ + public static class StyleCellsRequest { + /** + * 工作表ID + */ + private String sheetId; + + /** + * 单元格范围的开始位置 + */ + private String startPosition; + + /** + * 单元格范围的结束位置 + */ + private String endPosition; + + /** + * 兼容旧版接口的完整range + */ + private String legacyRange; + + /** + * 单元格样式 + */ + private Style style; + + /** + * 获取要设置样式的单元格范围 + * + * @return 单元格范围,格式为 !<开始位置>:<结束位置> + */ + public String getRange() { + if (legacyRange != null && !legacyRange.isEmpty()) { + return legacyRange; + } + + if (sheetId != null && startPosition != null && endPosition != null) { + return sheetId + "!" + startPosition + ":" + endPosition; + } + + return null; + } + + /** + * 获取工作表ID + * + * @return 工作表ID + */ + public String getSheetId() { + return sheetId; + } + + /** + * 设置工作表ID + * + * @param sheetId 工作表ID + */ + public void setSheetId(String sheetId) { + this.sheetId = sheetId; + } + + /** + * 获取单元格范围的开始位置 + * + * @return 开始位置 + */ + public String getStartPosition() { + return startPosition; + } + + /** + * 设置单元格范围的开始位置 + * + * @param startPosition 开始位置,如A1 + */ + public void setStartPosition(String startPosition) { + this.startPosition = startPosition; + } + + /** + * 获取单元格范围的结束位置 + * + * @return 结束位置 + */ + public String getEndPosition() { + return endPosition; + } + + /** + * 设置单元格范围的结束位置 + * + * @param endPosition 结束位置,如B2 + */ + public void setEndPosition(String endPosition) { + this.endPosition = endPosition; + } + + /** + * 设置兼容旧版接口的完整range + * + * @param legacyRange 完整range + */ + public void setLegacyRange(String legacyRange) { + this.legacyRange = legacyRange; + } + + /** + * 获取单元格样式 + * + * @return 单元格样式 + */ + public Style getStyle() { + return style; + } + + /** + * 设置单元格样式 + * + * @param style 单元格样式 + */ + public void setStyle(Style style) { + this.style = style; + } + } + + /** + * 单元格样式 + */ + public static class Style { + private Font font; + private Integer textDecoration; + private String formatter; + private Integer hAlign; + private Integer vAlign; + private String foreColor; + private String backColor; + private String borderType; + private String borderColor; + private Boolean clean; + + /** + * 获取字体样式 + * + * @return 字体样式 + */ + public Font getFont() { + return font; + } + + /** + * 设置字体样式 + * + * @param font 字体样式 + */ + public void setFont(Font font) { + this.font = font; + } + + /** + * 获取文本装饰 + * + * @return 文本装饰,0:默认样式,1:下划线,2:删除线,3:下划线和删除线 + */ + public Integer getTextDecoration() { + return textDecoration; + } + + /** + * 设置文本装饰 + * + * @param textDecoration 文本装饰,0:默认样式,1:下划线,2:删除线,3:下划线和删除线 + */ + public void setTextDecoration(Integer textDecoration) { + this.textDecoration = textDecoration; + } + + /** + * 获取数字格式 + * + * @return 数字格式 + */ + public String getFormatter() { + return formatter; + } + + /** + * 设置数字格式 + * + * @param formatter 数字格式 + */ + public void setFormatter(String formatter) { + this.formatter = formatter; + } + + /** + * 获取水平对齐方式 + * + * @return 水平对齐方式,0:左对齐,1:中对齐,2:右对齐 + */ + public Integer getHAlign() { + return hAlign; + } + + /** + * 设置水平对齐方式 + * + * @param hAlign 水平对齐方式,0:左对齐,1:中对齐,2:右对齐 + */ + public void setHAlign(Integer hAlign) { + this.hAlign = hAlign; + } + + /** + * 获取垂直对齐方式 + * + * @return 垂直对齐方式,0:上对齐,1:中对齐,2:下对齐 + */ + public Integer getVAlign() { + return vAlign; + } + + /** + * 设置垂直对齐方式 + * + * @param vAlign 垂直对齐方式,0:上对齐,1:中对齐,2:下对齐 + */ + public void setVAlign(Integer vAlign) { + this.vAlign = vAlign; + } + + /** + * 获取字体颜色 + * + * @return 字体颜色,十六进制颜色代码 + */ + public String getForeColor() { + return foreColor; + } + + /** + * 设置字体颜色 + * + * @param foreColor 字体颜色,十六进制颜色代码 + */ + public void setForeColor(String foreColor) { + this.foreColor = foreColor; + } + + /** + * 获取背景颜色 + * + * @return 背景颜色,十六进制颜色代码 + */ + public String getBackColor() { + return backColor; + } + + /** + * 设置背景颜色 + * + * @param backColor 背景颜色,十六进制颜色代码 + */ + public void setBackColor(String backColor) { + this.backColor = backColor; + } + + /** + * 获取边框类型 + * + * @return 边框类型 + */ + public String getBorderType() { + return borderType; + } + + /** + * 设置边框类型 + * + * @param borderType + * 边框类型,可选值:FULL_BORDER、OUTER_BORDER、INNER_BORDER、NO_BORDER、LEFT_BORDER、RIGHT_BORDER、TOP_BORDER、BOTTOM_BORDER + */ + public void setBorderType(String borderType) { + this.borderType = borderType; + } + + /** + * 获取边框颜色 + * + * @return 边框颜色,十六进制颜色代码 + */ + public String getBorderColor() { + return borderColor; + } + + /** + * 设置边框颜色 + * + * @param borderColor 边框颜色,十六进制颜色代码 + */ + public void setBorderColor(String borderColor) { + this.borderColor = borderColor; + } + + /** + * 获取是否清除所有格式 + * + * @return 是否清除所有格式 + */ + public Boolean getClean() { + return clean; + } + + /** + * 设置是否清除所有格式 + * + * @param clean 是否清除所有格式 + */ + public void setClean(Boolean clean) { + this.clean = clean; + } + } + + /** + * 字体样式 + */ + public static class Font { + private Boolean bold; + private Boolean italic; + private String fontSize; + private Boolean clean; + + /** + * 获取是否加粗 + * + * @return 是否加粗 + */ + public Boolean getBold() { + return bold; + } + + /** + * 设置是否加粗 + * + * @param bold 是否加粗 + */ + public void setBold(Boolean bold) { + this.bold = bold; + } + + /** + * 获取是否斜体 + * + * @return 是否斜体 + */ + public Boolean getItalic() { + return italic; + } + + /** + * 设置是否斜体 + * + * @param italic 是否斜体 + */ + public void setItalic(Boolean italic) { + this.italic = italic; + } + + /** + * 获取字体大小 + * + * @return 字体大小,如10pt/1.5 + */ + public String getFontSize() { + return fontSize; + } + + /** + * 设置字体大小 + * + * @param fontSize 字体大小,如10pt/1.5 + */ + public void setFontSize(String fontSize) { + this.fontSize = fontSize; + } + + /** + * 获取是否清除字体格式 + * + * @return 是否清除字体格式 + */ + public Boolean getClean() { + return clean; + } + + /** + * 设置是否清除字体格式 + * + * @param clean 是否清除字体格式 + */ + public void setClean(Boolean clean) { + this.clean = clean; + } + } + + /** + * 合并单元格请求体(用于API请求) + */ + private static class MergeCellsRequestBody { + private final String range; + private final String mergeType; + + /** + * 构造函数 + * + * @param range 单元格范围 + * @param mergeType 合并方式 + */ + public MergeCellsRequestBody(String range, String mergeType) { + this.range = range; + this.mergeType = mergeType; + } + + /** + * 获取单元格范围 + * + * @return 单元格范围 + */ + public String getRange() { + return range; + } + + /** + * 获取合并方式 + * + * @return 合并方式 + */ + public String getMergeType() { + return mergeType; + } + } + + /** + * 拆分单元格请求体(用于API请求) + */ + private static class UnmergeCellsRequestBody { + private final String range; + + /** + * 构造函数 + * + * @param range 单元格范围 + */ + public UnmergeCellsRequestBody(String range) { + this.range = range; + } + + /** + * 获取单元格范围 + * + * @return 单元格范围 + */ + public String getRange() { + return range; + } + } + + /** + * 设置单元格样式请求体(用于API请求) + */ + private static class StyleCellsRequestBody { + private final AppendStyle appendStyle; + + /** + * 构造函数 + * + * @param range 单元格范围 + * @param style 单元格样式 + */ + public StyleCellsRequestBody(String range, Style style) { + this.appendStyle = new AppendStyle(range, style); + } + + /** + * 获取appendStyle + * + * @return appendStyle + */ + public AppendStyle getAppendStyle() { + return appendStyle; + } + + /** + * 设置单元格样式请求的appendStyle部分 + */ + private static class AppendStyle { + private final String range; + private final Style style; + + /** + * 构造函数 + * + * @param range 单元格范围 + * @param style 单元格样式 + */ + public AppendStyle(String range, Style style) { + this.range = range; + this.style = style; + } + + /** + * 获取单元格范围 + * + * @return 单元格范围 + */ + public String getRange() { + return range; + } + + /** + * 获取单元格样式 + * + * @return 单元格样式 + */ + public Style getStyle() { + return style; + } + } + } + + /** + * 设置单元格样式的构建器 用于构建设置单元格样式的请求 + */ + public static class StyleCellsBuilder { + private final CellRequest request; + private final StyleCellsRequest styleCells; + private final Style style; + private Font font; + + public StyleCellsBuilder() { + request = new CellRequest(); + styleCells = new StyleCellsRequest(); + style = new Style(); + styleCells.setStyle(style); + request.setStyleCells(styleCells); + } + + /** + * 设置要设置样式的单元格所在的工作表ID + * + * @param sheetId 工作表ID + * @return 当前构建器 + */ + public StyleCellsBuilder sheetId(String sheetId) { + styleCells.setSheetId(sheetId); + return this; + } + + /** + * 设置要设置样式的单元格范围的开始位置 + * + * @param startPosition 开始位置,如A1 + * @return 当前构建器 + */ + public StyleCellsBuilder startPosition(String startPosition) { + styleCells.setStartPosition(startPosition); + return this; + } + + /** + * 设置要设置样式的单元格范围的结束位置 + * + * @param endPosition 结束位置,如B2 + * @return 当前构建器 + */ + public StyleCellsBuilder endPosition(String endPosition) { + styleCells.setEndPosition(endPosition); + return this; + } + + /** + * 设置是否加粗 + * + * @param bold 是否加粗 + * @return 当前构建器 + */ + public StyleCellsBuilder bold(Boolean bold) { + if (font == null) { + font = new Font(); + style.setFont(font); + } + font.setBold(bold); + return this; + } + + /** + * 设置是否斜体 + * + * @param italic 是否斜体 + * @return 当前构建器 + */ + public StyleCellsBuilder italic(Boolean italic) { + if (font == null) { + font = new Font(); + style.setFont(font); + } + font.setItalic(italic); + return this; + } + + /** + * 设置字体大小 + * + * @param fontSize 字体大小,如10pt/1.5 + * @return 当前构建器 + */ + public StyleCellsBuilder fontSize(String fontSize) { + if (font == null) { + font = new Font(); + style.setFont(font); + } + font.setFontSize(fontSize); + return this; + } + + /** + * 设置是否清除字体格式 + * + * @param clean 是否清除字体格式 + * @return 当前构建器 + */ + public StyleCellsBuilder fontClean(Boolean clean) { + if (font == null) { + font = new Font(); + style.setFont(font); + } + font.setClean(clean); + return this; + } + + /** + * 设置文本装饰 + * + * @param textDecoration 文本装饰,0:默认样式,1:下划线,2:删除线,3:下划线和删除线 + * @return 当前构建器 + */ + public StyleCellsBuilder textDecoration(Integer textDecoration) { + style.setTextDecoration(textDecoration); + return this; + } + + /** + * 设置数字格式 + * + * @param formatter 数字格式 + * @return 当前构建器 + */ + public StyleCellsBuilder formatter(String formatter) { + style.setFormatter(formatter); + return this; + } + + /** + * 设置水平对齐方式 + * + * @param hAlign 水平对齐方式,0:左对齐,1:中对齐,2:右对齐 + * @return 当前构建器 + */ + public StyleCellsBuilder hAlign(Integer hAlign) { + style.setHAlign(hAlign); + return this; + } + + /** + * 设置垂直对齐方式 + * + * @param vAlign 垂直对齐方式,0:上对齐,1:中对齐,2:下对齐 + * @return 当前构建器 + */ + public StyleCellsBuilder vAlign(Integer vAlign) { + style.setVAlign(vAlign); + return this; + } + + /** + * 设置字体颜色 + * + * @param foreColor 字体颜色,十六进制颜色代码 + * @return 当前构建器 + */ + public StyleCellsBuilder foreColor(String foreColor) { + style.setForeColor(foreColor); + return this; + } + + /** + * 设置背景颜色 + * + * @param backColor 背景颜色,十六进制颜色代码 + * @return 当前构建器 + */ + public StyleCellsBuilder backColor(String backColor) { + style.setBackColor(backColor); + return this; + } + + /** + * 设置边框类型 + * + * @param borderType + * 边框类型,可选值:FULL_BORDER、OUTER_BORDER、INNER_BORDER、NO_BORDER、LEFT_BORDER、RIGHT_BORDER、TOP_BORDER、BOTTOM_BORDER + * @return 当前构建器 + */ + public StyleCellsBuilder borderType(String borderType) { + style.setBorderType(borderType); + return this; + } + + /** + * 设置边框颜色 + * + * @param borderColor 边框颜色,十六进制颜色代码 + * @return 当前构建器 + */ + public StyleCellsBuilder borderColor(String borderColor) { + style.setBorderColor(borderColor); + return this; + } + + /** + * 设置是否清除所有格式 + * + * @param clean 是否清除所有格式 + * @return 当前构建器 + */ + public StyleCellsBuilder clean(Boolean clean) { + style.setClean(clean); + return this; + } + + /** + * 构建设置单元格样式请求 + * + * @return 单元格操作请求 + */ + public CellRequest build() { + return request; + } + } + + /** + * 批量设置单元格样式请求 + */ + public static class StyleBatchUpdateRequest { + private List data; + + public StyleBatchUpdateRequest() { + this.data = new ArrayList<>(); + } + + public List getData() { + return data; + } + + public void setData(List data) { + this.data = data; + } + + /** + * 创建批量设置单元格样式请求的构建器 + * + * @return 批量设置单元格样式请求的构建器 + */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * 批量设置单元格样式请求的构建器 + */ + public static class Builder { + private final StyleBatchUpdateRequest request; + + public Builder() { + request = new StyleBatchUpdateRequest(); + } + + /** + * 添加一组样式设置 + * + * @param styleBatchData 样式设置数据 + * @return 当前构建器 + */ + public Builder addStyleBatch(StyleBatchData styleBatchData) { + request.data.add(styleBatchData); + return this; + } + + /** + * 构建批量设置单元格样式请求 + * + * @return 批量设置单元格样式请求 + */ + public StyleBatchUpdateRequest build() { + return request; + } + } + } + + /** + * 批量设置单元格样式数据 + */ + public static class StyleBatchData { + private List ranges; + private Style style; + + public StyleBatchData() { + this.ranges = new ArrayList<>(); + } + + public List getRanges() { + return ranges; + } + + public void setRanges(List ranges) { + this.ranges = ranges; + } + + public Style getStyle() { + return style; + } + + public void setStyle(Style style) { + this.style = style; + } + + /** + * 创建批量设置单元格样式数据的构建器 + * + * @return 批量设置单元格样式数据的构建器 + */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * 批量设置单元格样式数据的构建器 + */ + public static class Builder { + private final StyleBatchData data; + private final Style style; + private Font font; + + public Builder() { + data = new StyleBatchData(); + style = new Style(); + data.setStyle(style); + } + + /** + * 添加要设置样式的单元格范围 + * + * @param range 单元格范围,格式为 !<开始位置>:<结束位置> + * @return 当前构建器 + */ + public Builder addRange(String range) { + data.ranges.add(range); + return this; + } + + /** + * 设置是否加粗 + * + * @param bold 是否加粗 + * @return 当前构建器 + */ + public Builder bold(Boolean bold) { + if (font == null) { + font = new Font(); + style.setFont(font); + } + font.setBold(bold); + return this; + } + + /** + * 设置是否斜体 + * + * @param italic 是否斜体 + * @return 当前构建器 + */ + public Builder italic(Boolean italic) { + if (font == null) { + font = new Font(); + style.setFont(font); + } + font.setItalic(italic); + return this; + } + + /** + * 设置字体大小 + * + * @param fontSize 字体大小,如10pt/1.5 + * @return 当前构建器 + */ + public Builder fontSize(String fontSize) { + if (font == null) { + font = new Font(); + style.setFont(font); + } + font.setFontSize(fontSize); + return this; + } + + /** + * 设置是否清除字体格式 + * + * @param clean 是否清除字体格式 + * @return 当前构建器 + */ + public Builder fontClean(Boolean clean) { + if (font == null) { + font = new Font(); + style.setFont(font); + } + font.setClean(clean); + return this; + } + + /** + * 设置文本装饰 + * + * @param textDecoration 文本装饰,0:默认样式,1:下划线,2:删除线,3:下划线和删除线 + * @return 当前构建器 + */ + public Builder textDecoration(Integer textDecoration) { + style.setTextDecoration(textDecoration); + return this; + } + + /** + * 设置数字格式 + * + * @param formatter 数字格式 + * @return 当前构建器 + */ + public Builder formatter(String formatter) { + style.setFormatter(formatter); + return this; + } + + /** + * 设置水平对齐方式 + * + * @param hAlign 水平对齐方式,0:左对齐,1:中对齐,2:右对齐 + * @return 当前构建器 + */ + public Builder hAlign(Integer hAlign) { + style.setHAlign(hAlign); + return this; + } + + /** + * 设置垂直对齐方式 + * + * @param vAlign 垂直对齐方式,0:上对齐,1:中对齐,2:下对齐 + * @return 当前构建器 + */ + public Builder vAlign(Integer vAlign) { + style.setVAlign(vAlign); + return this; + } + + /** + * 设置字体颜色 + * + * @param foreColor 字体颜色,十六进制颜色代码 + * @return 当前构建器 + */ + public Builder foreColor(String foreColor) { + style.setForeColor(foreColor); + return this; + } + + /** + * 设置背景颜色 + * + * @param backColor 背景颜色,十六进制颜色代码 + * @return 当前构建器 + */ + public Builder backColor(String backColor) { + style.setBackColor(backColor); + return this; + } + + /** + * 设置边框类型 + * + * @param borderType + * 边框类型,可选值:FULL_BORDER、OUTER_BORDER、INNER_BORDER、NO_BORDER、LEFT_BORDER、RIGHT_BORDER、TOP_BORDER、BOTTOM_BORDER + * @return 当前构建器 + */ + public Builder borderType(String borderType) { + style.setBorderType(borderType); + return this; + } + + /** + * 设置边框颜色 + * + * @param borderColor 边框颜色,十六进制颜色代码 + * @return 当前构建器 + */ + public Builder borderColor(String borderColor) { + style.setBorderColor(borderColor); + return this; + } + + /** + * 设置是否清除所有格式 + * + * @param clean 是否清除所有格式 + * @return 当前构建器 + */ + public Builder clean(Boolean clean) { + style.setClean(clean); + return this; + } + + /** + * 构建批量设置单元格样式数据 + * + * @return 批量设置单元格样式数据 + */ + public StyleBatchData build() { + return data; + } + } + } + + /** + * 批量设置单元格样式请求 + */ + public static class StyleCellsBatchRequest { + @Deprecated + private List ranges; + private List cellRanges; + private Style style; + private String type; + private String params; + + public StyleCellsBatchRequest() { + this.ranges = new ArrayList<>(); + this.cellRanges = new ArrayList<>(); + } + + /** + * 获取单元格范围列表(用于API请求) + * + * @return 单元格范围列表 + */ + public List getRanges() { + // 如果有新的结构化范围,优先使用它们 + if (cellRanges != null && !cellRanges.isEmpty()) { + List result = new ArrayList<>(); + for (CellRange cellRange : cellRanges) { + result.add(cellRange.getRange()); + } + return result; + } + return ranges; + } + + /** + * 获取单元格结构化范围列表 + * + * @return 单元格结构化范围列表 + */ + public List getCellRanges() { + return cellRanges; + } + + /** + * 设置单元格结构化范围列表 + * + * @param cellRanges 单元格结构化范围列表 + */ + public void setCellRanges(List cellRanges) { + this.cellRanges = cellRanges; + } + + /** + * 添加单元格结构化范围 + * + * @param cellRange 单元格结构化范围 + */ + public void addCellRange(CellRange cellRange) { + if (this.cellRanges == null) { + this.cellRanges = new ArrayList<>(); + } + this.cellRanges.add(cellRange); + } + + /** + * 添加单元格结构化范围 + * + * @param sheetId 工作表ID + * @param startPosition 开始位置 + * @param endPosition 结束位置 + */ + public void addCellRange(String sheetId, String startPosition, String endPosition) { + addCellRange(new CellRange(sheetId, startPosition, endPosition)); + } + + public Style getStyle() { + return style; + } + + public void setStyle(Style style) { + this.style = style; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getParams() { + return params; + } + public void setParams(String params) { + this.params = params; + } + } + + /** + * 单元格范围 + */ + public static class CellRange { + private String sheetId; + private String startPosition; + private String endPosition; + + /** + * 默认构造函数 + */ + public CellRange() {} + + /** + * 构造函数 + * + * @param sheetId 工作表ID + * @param startPosition 开始位置 + * @param endPosition 结束位置 + */ + public CellRange(String sheetId, String startPosition, String endPosition) { + this.sheetId = sheetId; + this.startPosition = startPosition; + this.endPosition = endPosition; + } + + /** + * 获取单元格范围 + * + * @return 单元格范围,格式为 !<开始位置>:<结束位置> + */ + public String getRange() { + if (sheetId != null && startPosition != null && endPosition != null) { + return sheetId + "!" + startPosition + ":" + endPosition; + } + return null; + } + + /** + * 从范围字符串解析 + * + * @param range 单元格范围,格式为 !<开始位置>:<结束位置> + * @return 单元格范围对象 + */ + public static CellRange fromRange(String range) { + CellRange cellRange = new CellRange(); + try { + String[] parts = range.split("!"); + cellRange.sheetId = parts[0]; + String[] positions = parts[1].split(":"); + cellRange.startPosition = positions[0]; + cellRange.endPosition = positions[1]; + } catch (Exception e) { + // 解析失败,返回空对象 + } + return cellRange; + } + + public String getSheetId() { + return sheetId; + } + + public void setSheetId(String sheetId) { + this.sheetId = sheetId; + } + + public String getStartPosition() { + return startPosition; + } + + public void setStartPosition(String startPosition) { + this.startPosition = startPosition; + } + + public String getEndPosition() { + return endPosition; + } + + public void setEndPosition(String endPosition) { + this.endPosition = endPosition; + } + } + + /** + * 批量设置单元格样式的构建器 用于构建批量设置单元格样式的请求 + */ + public static class StyleCellsBatchBuilder { + private final CellRequest request; + private final StyleCellsBatchRequest styleCellsBatch; + private final Style style; + private Font font; + + public StyleCellsBatchBuilder() { + request = new CellRequest(); + styleCellsBatch = new StyleCellsBatchRequest(); + style = new Style(); + styleCellsBatch.setStyle(style); + request.setStyleCellsBatch(styleCellsBatch); + } + + public StyleCellsBatchBuilder setReqType(String reqType) { + styleCellsBatch.setType(reqType); + return this; + } + + public StyleCellsBatchBuilder setParams(String params) { + styleCellsBatch.setParams(params); + return this; + } + + /** + * 添加要设置样式的单元格范围 + * + * @param sheetId 工作表ID + * @param startPosition 开始位置,如A1 + * @param endPosition 结束位置,如B2 + * @return 当前构建器 + */ + public StyleCellsBatchBuilder addRange(String sheetId, String startPosition, String endPosition) { + styleCellsBatch.addCellRange(sheetId, startPosition, endPosition); + return this; + } + + /** + * 设置是否加粗 + * + * @param bold 是否加粗 + * @return 当前构建器 + */ + public StyleCellsBatchBuilder bold(Boolean bold) { + if (font == null) { + font = new Font(); + style.setFont(font); + } + font.setBold(bold); + return this; + } + + /** + * 设置是否斜体 + * + * @param italic 是否斜体 + * @return 当前构建器 + */ + public StyleCellsBatchBuilder italic(Boolean italic) { + if (font == null) { + font = new Font(); + style.setFont(font); + } + font.setItalic(italic); + return this; + } + + /** + * 设置字体大小 + * + * @param fontSize 字体大小,如10pt/1.5 + * @return 当前构建器 + */ + public StyleCellsBatchBuilder fontSize(String fontSize) { + if (font == null) { + font = new Font(); + style.setFont(font); + } + font.setFontSize(fontSize); + return this; + } + + /** + * 设置是否清除字体格式 + * + * @param clean 是否清除字体格式 + * @return 当前构建器 + */ + public StyleCellsBatchBuilder fontClean(Boolean clean) { + if (font == null) { + font = new Font(); + style.setFont(font); + } + font.setClean(clean); + return this; + } + + /** + * 设置文本装饰 + * + * @param textDecoration 文本装饰,0:默认样式,1:下划线,2:删除线,3:下划线和删除线 + * @return 当前构建器 + */ + public StyleCellsBatchBuilder textDecoration(Integer textDecoration) { + style.setTextDecoration(textDecoration); + return this; + } + + /** + * 设置数字格式 + * + * @param formatter 数字格式 + * @return 当前构建器 + */ + public StyleCellsBatchBuilder formatter(String formatter) { + style.setFormatter(formatter); + return this; + } + + /** + * 设置水平对齐方式 + * + * @param hAlign 水平对齐方式,0:左对齐,1:中对齐,2:右对齐 + * @return 当前构建器 + */ + public StyleCellsBatchBuilder hAlign(Integer hAlign) { + style.setHAlign(hAlign); + return this; + } + + /** + * 设置垂直对齐方式 + * + * @param vAlign 垂直对齐方式,0:上对齐,1:中对齐,2:下对齐 + * @return 当前构建器 + */ + public StyleCellsBatchBuilder vAlign(Integer vAlign) { + style.setVAlign(vAlign); + return this; + } + + /** + * 设置字体颜色 + * + * @param foreColor 字体颜色,十六进制颜色代码 + * @return 当前构建器 + */ + public StyleCellsBatchBuilder foreColor(String foreColor) { + style.setForeColor(foreColor); + return this; + } + + /** + * 设置背景颜色 + * + * @param backColor 背景颜色,十六进制颜色代码 + * @return 当前构建器 + */ + public StyleCellsBatchBuilder backColor(String backColor) { + style.setBackColor(backColor); + return this; + } + + /** + * 设置边框类型 + * + * @param borderType + * 边框类型,可选值:FULL_BORDER、OUTER_BORDER、INNER_BORDER、NO_BORDER、LEFT_BORDER、RIGHT_BORDER、TOP_BORDER、BOTTOM_BORDER + * @return 当前构建器 + */ + public StyleCellsBatchBuilder borderType(String borderType) { + style.setBorderType(borderType); + return this; + } + + /** + * 设置边框颜色 + * + * @param borderColor 边框颜色,十六进制颜色代码 + * @return 当前构建器 + */ + public StyleCellsBatchBuilder borderColor(String borderColor) { + style.setBorderColor(borderColor); + return this; + } + + /** + * 设置是否清除所有格式 + * + * @param clean 是否清除所有格式 + * @return 当前构建器 + */ + public StyleCellsBatchBuilder clean(Boolean clean) { + style.setClean(clean); + return this; + } + + /** + * 构建批量设置单元格样式请求 + * + * @return 单元格操作请求 + */ + public CellRequest build() { + return request; + } + } +} \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/service/CustomDataValidationService.java b/src/main/java/cn/isliu/core/service/CustomDataValidationService.java new file mode 100644 index 0000000..d86fc9f --- /dev/null +++ b/src/main/java/cn/isliu/core/service/CustomDataValidationService.java @@ -0,0 +1,1094 @@ +package cn.isliu.core.service; + + +import okhttp3.Request; +import okhttp3.RequestBody; + +import cn.isliu.core.client.FeishuApiClient; +import cn.isliu.core.client.FeishuClient; +import cn.isliu.core.pojo.ApiResponse; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * 自定义数据验证服务 提供官方SDK未覆盖的数据验证API + */ +public class CustomDataValidationService extends FeishuApiClient { + + /** + * 构造函数 + * + * @param feishuClient 飞书客户端 + */ + public CustomDataValidationService(FeishuClient feishuClient) { + super(feishuClient); + } + + /** + * 批量处理数据验证请求 + * + * @param spreadsheetToken 电子表格Token + * @param request 批量处理请求 + * @return 批量处理响应 + * @throws IOException 请求异常 + */ + public ApiResponse dataValidationBatchUpdate(String spreadsheetToken, DataValidationBatchUpdateRequest request) + throws IOException { + List requests = request.getRequests(); + ApiResponse response = null; + + // 如果没有请求,返回空响应 + if (requests == null || requests.isEmpty()) { + ApiResponse emptyResponse = new ApiResponse(); + emptyResponse.setCode(400); + emptyResponse.setMsg("No data validation operations found"); + return emptyResponse; + } + + // 依次处理每个请求 + for (DataValidationRequest validationRequest : requests) { + // 处理查询下拉列表请求 + if (validationRequest.getQueryValidation() != null) { + QueryValidationRequest queryValidation = validationRequest.getQueryValidation(); + + // 验证请求参数 + if (queryValidation.getRange() == null || queryValidation.getRange().isEmpty()) { + ApiResponse errorResponse = new ApiResponse(); + errorResponse.setCode(400); + errorResponse.setMsg("Range cannot be empty for data validation query"); + return errorResponse; + } + + // 构建基本URL + String baseUrl = BASE_URL + "/sheets/v2/spreadsheets/" + spreadsheetToken + "/dataValidation"; + + // 构建查询参数 + StringBuilder urlBuilder = new StringBuilder(baseUrl); + urlBuilder.append("?range=") + .append(URLEncoder.encode(queryValidation.getRange(), StandardCharsets.UTF_8.toString())); + + // 添加dataValidationType参数 + urlBuilder.append("&dataValidationType=list"); + + String url = urlBuilder.toString(); + Request httpRequest = createAuthenticatedRequest(url, "GET", null).build(); + response = executeRequest(httpRequest, ApiResponse.class); + + // 如果请求失败,中断后续请求 + if (!response.success()) { + return response; + } + } + // 处理删除下拉列表请求 + else if (validationRequest.getDeleteValidation() != null) { + DeleteValidationRequest deleteValidation = validationRequest.getDeleteValidation(); + + // 验证请求参数 + if (deleteValidation.getRange() == null || deleteValidation.getRange().isEmpty()) { + ApiResponse errorResponse = new ApiResponse(); + errorResponse.setCode(400); + errorResponse.setMsg("Range cannot be empty for data validation delete"); + return errorResponse; + } + + if (deleteValidation.getDataValidationIds() == null + || deleteValidation.getDataValidationIds().isEmpty()) { + ApiResponse errorResponse = new ApiResponse(); + errorResponse.setCode(400); + errorResponse.setMsg("DataValidationIds cannot be empty for data validation delete"); + return errorResponse; + } + + // 构建基本URL + String url = BASE_URL + "/sheets/v2/spreadsheets/" + spreadsheetToken + "/dataValidation"; + + // 构建删除请求体 + DeleteValidationRequestBody requestBody = new DeleteValidationRequestBody(); + DeleteValidationRange validationRange = new DeleteValidationRange(); + validationRange.setRange(deleteValidation.getRange()); + validationRange.setDataValidationIds(deleteValidation.getDataValidationIds()); + requestBody.getDataValidationRanges().add(validationRange); + + RequestBody body = RequestBody.create(gson.toJson(requestBody), JSON_MEDIA_TYPE); + Request httpRequest = createAuthenticatedRequest(url, "DELETE", body).build(); + response = executeRequest(httpRequest, ApiResponse.class); + + // 如果请求失败,中断后续请求 + if (!response.success()) { + return response; + } + } + // 处理设置下拉列表请求 + else if (validationRequest.getRange() != null && "list".equals(validationRequest.getDataValidationType())) { + String url = BASE_URL + "/sheets/v2/spreadsheets/" + spreadsheetToken + "/dataValidation"; + + RequestBody body = RequestBody.create(gson.toJson(validationRequest), JSON_MEDIA_TYPE); + Request httpRequest = createAuthenticatedRequest(url, "POST", body).build(); + response = executeRequest(httpRequest, ApiResponse.class); + + // 如果请求失败,中断后续请求 + if (!response.success()) { + return response; + } + } + // 处理更新下拉列表请求 + else if (validationRequest.getSheetId() != null && validationRequest.getDataValidationId() != null + && "list".equals(validationRequest.getDataValidationType())) { + String url = BASE_URL + "/sheets/v2/spreadsheets/" + spreadsheetToken + "/dataValidation/" + + validationRequest.getSheetId() + "/" + validationRequest.getDataValidationId(); + + // 创建新的请求体,不包含sheetId和dataValidationId字段 + DataValidationRequest requestBody = new DataValidationRequest(); + requestBody.setDataValidationType(validationRequest.getDataValidationType()); + requestBody.setDataValidation(validationRequest.getDataValidation()); + + RequestBody body = RequestBody.create(gson.toJson(requestBody), JSON_MEDIA_TYPE); + Request httpRequest = createAuthenticatedRequest(url, "PUT", body).build(); + response = executeRequest(httpRequest, ApiResponse.class); + + // 如果请求失败,中断后续请求 + if (!response.success()) { + return response; + } + } + // 这里可以添加其他数据验证类型的处理 + } + + // 如果所有请求都成功处理,返回最后一个成功的响应 + // 如果没有处理任何请求(没有有效的操作类型),返回错误响应 + if (response == null) { + ApiResponse errorResponse = new ApiResponse(); + errorResponse.setCode(400); + errorResponse.setMsg("No valid data validation operation found"); + return errorResponse; + } + + return response; + } + + /** + * 删除下拉列表设置请求体 + */ + private static class DeleteValidationRequestBody { + private List dataValidationRanges; + + public DeleteValidationRequestBody() { + this.dataValidationRanges = new ArrayList<>(); + } + + public List getDataValidationRanges() { + return dataValidationRanges; + } + + public void setDataValidationRanges(List dataValidationRanges) { + this.dataValidationRanges = dataValidationRanges; + } + } + + /** + * 删除下拉列表设置范围 + */ + private static class DeleteValidationRange { + private String range; + private List dataValidationIds; + + public String getRange() { + return range; + } + + public void setRange(String range) { + this.range = range; + } + + public List getDataValidationIds() { + return dataValidationIds; + } + + public void setDataValidationIds(List dataValidationIds) { + this.dataValidationIds = dataValidationIds; + } + } + + /** + * 批量处理数据验证请求 + */ + public static class DataValidationBatchUpdateRequest { + private List requests; + + public DataValidationBatchUpdateRequest() { + this.requests = new ArrayList<>(); + } + + public List getRequests() { + return requests; + } + + public void setRequests(List requests) { + this.requests = requests; + } + + /** + * 创建批量处理数据验证请求的构建器 + * + * @return 批量处理数据验证请求的构建器 + */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * 批量处理数据验证请求的构建器 + */ + public static class Builder { + private final DataValidationBatchUpdateRequest request; + + public Builder() { + request = new DataValidationBatchUpdateRequest(); + } + + /** + * 添加一个数据验证请求 + * + * @param validationRequest 数据验证请求 + * @return 当前构建器 + */ + public Builder addRequest(DataValidationRequest validationRequest) { + request.requests.add(validationRequest); + return this; + } + + /** + * 构建批量处理数据验证请求 + * + * @return 批量处理数据验证请求 + */ + public DataValidationBatchUpdateRequest build() { + return request; + } + } + } + + /** + * 数据验证请求 + */ + public static class DataValidationRequest { + private String range; + private String dataValidationType; + private DataValidation dataValidation; + private String sheetId; + private Integer dataValidationId; + private QueryValidationRequest queryValidation; + private DeleteValidationRequest deleteValidation; + + /** + * 获取单元格范围 + * + * @return 单元格范围 + */ + public String getRange() { + return range; + } + + /** + * 设置单元格范围 + * + * @param range 单元格范围,格式为 !<开始位置>:<结束位置> + */ + public void setRange(String range) { + this.range = range; + } + + /** + * 获取数据验证类型 + * + * @return 数据验证类型 + */ + public String getDataValidationType() { + return dataValidationType; + } + + /** + * 设置数据验证类型 + * + * @param dataValidationType 数据验证类型 + */ + public void setDataValidationType(String dataValidationType) { + this.dataValidationType = dataValidationType; + } + + /** + * 获取数据验证规则 + * + * @return 数据验证规则 + */ + public DataValidation getDataValidation() { + return dataValidation; + } + + /** + * 设置数据验证规则 + * + * @param dataValidation 数据验证规则 + */ + public void setDataValidation(DataValidation dataValidation) { + this.dataValidation = dataValidation; + } + + /** + * 获取工作表ID + * + * @return 工作表ID + */ + public String getSheetId() { + return sheetId; + } + + /** + * 设置工作表ID + * + * @param sheetId 工作表ID + */ + public void setSheetId(String sheetId) { + this.sheetId = sheetId; + } + + /** + * 获取下拉列表ID + * + * @return 下拉列表ID + */ + public Integer getDataValidationId() { + return dataValidationId; + } + + /** + * 设置下拉列表ID + * + * @param dataValidationId 下拉列表ID + */ + public void setDataValidationId(Integer dataValidationId) { + this.dataValidationId = dataValidationId; + } + + /** + * 获取查询下拉列表设置请求 + * + * @return 查询下拉列表设置请求 + */ + public QueryValidationRequest getQueryValidation() { + return queryValidation; + } + + /** + * 设置查询下拉列表设置请求 + * + * @param queryValidation 查询下拉列表设置请求 + */ + public void setQueryValidation(QueryValidationRequest queryValidation) { + this.queryValidation = queryValidation; + } + + /** + * 获取删除下拉列表设置请求 + * + * @return 删除下拉列表设置请求 + */ + public DeleteValidationRequest getDeleteValidation() { + return deleteValidation; + } + + /** + * 设置删除下拉列表设置请求 + * + * @param deleteValidation 删除下拉列表设置请求 + */ + public void setDeleteValidation(DeleteValidationRequest deleteValidation) { + this.deleteValidation = deleteValidation; + } + + /** + * 创建设置下拉列表的请求构建器 + * + * @return 设置下拉列表的构建器 + */ + public static ListDataValidationBuilder listValidation() { + return new ListDataValidationBuilder(); + } + + /** + * 创建更新下拉列表的请求构建器 + * + * @return 更新下拉列表的构建器 + */ + public static UpdateListDataValidationBuilder updateListValidation() { + return new UpdateListDataValidationBuilder(); + } + + /** + * 创建查询下拉列表的请求构建器 + * + * @return 查询下拉列表的构建器 + */ + public static QueryListDataValidationBuilder queryListValidation() { + return new QueryListDataValidationBuilder(); + } + + /** + * 创建删除下拉列表的请求构建器 + * + * @return 删除下拉列表的构建器 + */ + public static DeleteListDataValidationBuilder deleteListValidation() { + return new DeleteListDataValidationBuilder(); + } + + /** + * 设置下拉列表的构建器 + */ + public static class ListDataValidationBuilder { + private final DataValidationRequest request; + private final DataValidation dataValidation; + private final DataValidationOptions options; + + public ListDataValidationBuilder() { + request = new DataValidationRequest(); + dataValidation = new DataValidation(); + options = new DataValidationOptions(); + + request.setDataValidationType("list"); + dataValidation.setOptions(options); + request.setDataValidation(dataValidation); + } + + /** + * 设置单元格范围 + * + * @param range 单元格范围,格式为 !<开始位置>:<结束位置> + * @return 当前构建器 + */ + public ListDataValidationBuilder range(String range) { + request.setRange(range); + return this; + } + + /** + * 设置单元格范围 + * + * @param sheetId 工作表ID + * @param startPosition 开始位置 + * @param endPosition 结束位置 + * @return 当前构建器 + */ + public ListDataValidationBuilder range(String sheetId, String startPosition, String endPosition) { + request.setRange(sheetId + "!" + startPosition + ":" + endPosition); + return this; + } + + /** + * 添加下拉选项值 + * + * @param value 选项值 + * @return 当前构建器 + */ + public ListDataValidationBuilder addValue(String value) { + dataValidation.getConditionValues().add(value); + return this; + } + + /** + * 添加下拉选项值 + * + * @param values 选项值列表 + * @return 当前构建器 + */ + public ListDataValidationBuilder addValues(List values) { + dataValidation.getConditionValues().addAll(values); + return this; + } + + /** + * 添加下拉选项值 + * + * @param values 选项值数组 + * @return 当前构建器 + */ + public ListDataValidationBuilder addValues(String... values) { + for (String value : values) { + dataValidation.getConditionValues().add(value); + } + return this; + } + + /** + * 设置是否支持多选 + * + * @param multipleValues 是否支持多选 + * @return 当前构建器 + */ + public ListDataValidationBuilder multipleValues(boolean multipleValues) { + options.setMultipleValues(multipleValues); + return this; + } + + /** + * 设置是否为下拉选项设置颜色 + * + * @param highlightValidData 是否为下拉选项设置颜色 + * @return 当前构建器 + */ + public ListDataValidationBuilder highlightValidData(boolean highlightValidData) { + options.setHighlightValidData(highlightValidData); + return this; + } + + /** + * 添加下拉选项颜色 + * + * @param color 颜色,格式为RGB 16进制,如"#fffd00" + * @return 当前构建器 + */ + public ListDataValidationBuilder addColor(String color) { + options.getColors().add(color); + return this; + } + + /** + * 添加下拉选项颜色 + * + * @param colors 颜色列表 + * @return 当前构建器 + */ + public ListDataValidationBuilder addColors(List colors) { + options.getColors().addAll(colors); + return this; + } + + /** + * 添加下拉选项颜色 + * + * @param colors 颜色数组 + * @return 当前构建器 + */ + public ListDataValidationBuilder addColors(String... colors) { + for (String color : colors) { + options.getColors().add(color); + } + return this; + } + + /** + * 构建设置下拉列表请求 + * + * @return 数据验证请求 + */ + public DataValidationRequest build() { + return request; + } + } + + /** + * 更新下拉列表的构建器 + */ + public static class UpdateListDataValidationBuilder { + private final DataValidationRequest request; + private final DataValidation dataValidation; + private final DataValidationOptions options; + + public UpdateListDataValidationBuilder() { + request = new DataValidationRequest(); + dataValidation = new DataValidation(); + options = new DataValidationOptions(); + + request.setDataValidationType("list"); + dataValidation.setOptions(options); + request.setDataValidation(dataValidation); + } + + /** + * 设置工作表ID和下拉列表ID + * + * @param sheetId 工作表ID + * @param dataValidationId 下拉列表ID + * @return 当前构建器 + */ + public UpdateListDataValidationBuilder target(String sheetId, int dataValidationId) { + request.setSheetId(sheetId); + request.setDataValidationId(dataValidationId); + return this; + } + + /** + * 添加下拉选项值 + * + * @param value 选项值 + * @return 当前构建器 + */ + public UpdateListDataValidationBuilder addValue(String value) { + dataValidation.getConditionValues().add(value); + return this; + } + + /** + * 添加下拉选项值 + * + * @param values 选项值列表 + * @return 当前构建器 + */ + public UpdateListDataValidationBuilder addValues(List values) { + dataValidation.getConditionValues().addAll(values); + return this; + } + + /** + * 添加下拉选项值 + * + * @param values 选项值数组 + * @return 当前构建器 + */ + public UpdateListDataValidationBuilder addValues(String... values) { + for (String value : values) { + dataValidation.getConditionValues().add(value); + } + return this; + } + + /** + * 设置下拉选项值(替换现有值) + * + * @param values 选项值列表 + * @return 当前构建器 + */ + public UpdateListDataValidationBuilder setValues(List values) { + dataValidation.setConditionValues(new ArrayList<>(values)); + return this; + } + + /** + * 设置下拉选项值(替换现有值) + * + * @param values 选项值数组 + * @return 当前构建器 + */ + public UpdateListDataValidationBuilder setValues(String... values) { + List valueList = new ArrayList<>(); + for (String value : values) { + valueList.add(value); + } + dataValidation.setConditionValues(valueList); + return this; + } + + /** + * 设置是否支持多选 + * + * @param multipleValues 是否支持多选 + * @return 当前构建器 + */ + public UpdateListDataValidationBuilder multipleValues(boolean multipleValues) { + options.setMultipleValues(multipleValues); + return this; + } + + /** + * 设置是否为下拉选项设置颜色 + * + * @param highlightValidData 是否为下拉选项设置颜色 + * @return 当前构建器 + */ + public UpdateListDataValidationBuilder highlightValidData(boolean highlightValidData) { + options.setHighlightValidData(highlightValidData); + return this; + } + + /** + * 添加下拉选项颜色 + * + * @param color 颜色,格式为RGB 16进制,如"#fffd00" + * @return 当前构建器 + */ + public UpdateListDataValidationBuilder addColor(String color) { + options.getColors().add(color); + return this; + } + + /** + * 添加下拉选项颜色 + * + * @param colors 颜色列表 + * @return 当前构建器 + */ + public UpdateListDataValidationBuilder addColors(List colors) { + options.getColors().addAll(colors); + return this; + } + + /** + * 添加下拉选项颜色 + * + * @param colors 颜色数组 + * @return 当前构建器 + */ + public UpdateListDataValidationBuilder addColors(String... colors) { + for (String color : colors) { + options.getColors().add(color); + } + return this; + } + + /** + * 设置下拉选项颜色(替换现有颜色) + * + * @param colors 颜色列表 + * @return 当前构建器 + */ + public UpdateListDataValidationBuilder setColors(List colors) { + options.setColors(new ArrayList<>(colors)); + return this; + } + + /** + * 设置下拉选项颜色(替换现有颜色) + * + * @param colors 颜色数组 + * @return 当前构建器 + */ + public UpdateListDataValidationBuilder setColors(String... colors) { + List colorList = new ArrayList<>(); + for (String color : colors) { + colorList.add(color); + } + options.setColors(colorList); + return this; + } + + /** + * 构建更新下拉列表请求 + * + * @return 数据验证请求 + */ + public DataValidationRequest build() { + return request; + } + } + + /** + * 查询下拉列表的构建器 + */ + public static class QueryListDataValidationBuilder { + private final DataValidationRequest request; + private final QueryValidationRequest queryValidation; + + public QueryListDataValidationBuilder() { + request = new DataValidationRequest(); + queryValidation = new QueryValidationRequest(); + request.setQueryValidation(queryValidation); + } + + /** + * 设置单元格范围 + * + * @param range 单元格范围,格式为 !<开始位置>:<结束位置> + * @return 当前构建器 + */ + public QueryListDataValidationBuilder range(String range) { + queryValidation.setRange(range); + return this; + } + + /** + * 设置单元格范围 + * + * @param sheetId 工作表ID + * @param startPosition 开始位置 + * @param endPosition 结束位置 + * @return 当前构建器 + */ + public QueryListDataValidationBuilder range(String sheetId, String startPosition, String endPosition) { + queryValidation.setRange(sheetId + "!" + startPosition + ":" + endPosition); + return this; + } + + /** + * 构建查询下拉列表请求 + * + * @return 数据验证请求 + */ + public DataValidationRequest build() { + return request; + } + } + + /** + * 删除下拉列表的构建器 + */ + public static class DeleteListDataValidationBuilder { + private final DataValidationRequest request; + private final DeleteValidationRequest deleteValidation; + + public DeleteListDataValidationBuilder() { + request = new DataValidationRequest(); + deleteValidation = new DeleteValidationRequest(); + request.setDeleteValidation(deleteValidation); + } + + /** + * 设置单元格范围 + * + * @param range 单元格范围,格式为 !<开始位置>:<结束位置> + * @return 当前构建器 + */ + public DeleteListDataValidationBuilder range(String range) { + deleteValidation.setRange(range); + return this; + } + + /** + * 设置单元格范围 + * + * @param sheetId 工作表ID + * @param startPosition 开始位置 + * @param endPosition 结束位置 + * @return 当前构建器 + */ + public DeleteListDataValidationBuilder range(String sheetId, String startPosition, String endPosition) { + deleteValidation.setRange(sheetId + "!" + startPosition + ":" + endPosition); + return this; + } + + /** + * 添加要删除的下拉列表ID + * + * @param dataValidationId 下拉列表ID + * @return 当前构建器 + */ + public DeleteListDataValidationBuilder addDataValidationId(int dataValidationId) { + deleteValidation.getDataValidationIds().add(dataValidationId); + return this; + } + + /** + * 添加要删除的下拉列表ID列表 + * + * @param dataValidationIds 下拉列表ID列表 + * @return 当前构建器 + */ + public DeleteListDataValidationBuilder addDataValidationIds(List dataValidationIds) { + deleteValidation.getDataValidationIds().addAll(dataValidationIds); + return this; + } + + /** + * 添加要删除的下拉列表ID数组 + * + * @param dataValidationIds 下拉列表ID数组 + * @return 当前构建器 + */ + public DeleteListDataValidationBuilder addDataValidationIds(Integer... dataValidationIds) { + for (Integer id : dataValidationIds) { + deleteValidation.getDataValidationIds().add(id); + } + return this; + } + + /** + * 构建删除下拉列表请求 + * + * @return 数据验证请求 + */ + public DataValidationRequest build() { + return request; + } + } + } + + /** + * 查询下拉列表设置请求 + */ + public static class QueryValidationRequest { + private String range; + + /** + * 获取单元格范围 + * + * @return 单元格范围 + */ + public String getRange() { + return range; + } + + /** + * 设置单元格范围 + * + * @param range 单元格范围,格式为 !<开始位置>:<结束位置> + */ + public void setRange(String range) { + this.range = range; + } + } + + /** + * 删除下拉列表设置请求 + */ + public static class DeleteValidationRequest { + private String range; + private List dataValidationIds; + + public DeleteValidationRequest() { + this.dataValidationIds = new ArrayList<>(); + } + + /** + * 获取单元格范围 + * + * @return 单元格范围 + */ + public String getRange() { + return range; + } + + /** + * 设置单元格范围 + * + * @param range 单元格范围,格式为 !<开始位置>:<结束位置> + */ + public void setRange(String range) { + this.range = range; + } + + /** + * 获取要删除的下拉列表ID列表 + * + * @return 下拉列表ID列表 + */ + public List getDataValidationIds() { + return dataValidationIds; + } + + /** + * 设置要删除的下拉列表ID列表 + * + * @param dataValidationIds 下拉列表ID列表 + */ + public void setDataValidationIds(List dataValidationIds) { + this.dataValidationIds = dataValidationIds; + } + } + + /** + * 数据验证规则 + */ + public static class DataValidation { + private List conditionValues; + private DataValidationOptions options; + + public DataValidation() { + this.conditionValues = new ArrayList<>(); + } + + /** + * 获取条件值列表 + * + * @return 条件值列表 + */ + public List getConditionValues() { + return conditionValues; + } + + /** + * 设置条件值列表 + * + * @param conditionValues 条件值列表 + */ + public void setConditionValues(List conditionValues) { + this.conditionValues = conditionValues; + } + + /** + * 获取选项配置 + * + * @return 选项配置 + */ + public DataValidationOptions getOptions() { + return options; + } + + /** + * 设置选项配置 + * + * @param options 选项配置 + */ + public void setOptions(DataValidationOptions options) { + this.options = options; + } + } + + /** + * 数据验证选项配置 + */ + public static class DataValidationOptions { + private Boolean multipleValues; + private Boolean highlightValidData; + private List colors; + + public DataValidationOptions() { + this.colors = new ArrayList<>(); + } + + /** + * 获取是否支持多选 + * + * @return 是否支持多选 + */ + public Boolean getMultipleValues() { + return multipleValues; + } + + /** + * 设置是否支持多选 + * + * @param multipleValues 是否支持多选 + */ + public void setMultipleValues(Boolean multipleValues) { + this.multipleValues = multipleValues; + } + + /** + * 获取是否为下拉选项设置颜色 + * + * @return 是否为下拉选项设置颜色 + */ + public Boolean getHighlightValidData() { + return highlightValidData; + } + + /** + * 设置是否为下拉选项设置颜色 + * + * @param highlightValidData 是否为下拉选项设置颜色 + */ + public void setHighlightValidData(Boolean highlightValidData) { + this.highlightValidData = highlightValidData; + } + + /** + * 获取颜色列表 + * + * @return 颜色列表 + */ + public List getColors() { + return colors; + } + + /** + * 设置颜色列表 + * + * @param colors 颜色列表 + */ + public void setColors(List colors) { + this.colors = colors; + } + } +} \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/service/CustomDimensionService.java b/src/main/java/cn/isliu/core/service/CustomDimensionService.java new file mode 100644 index 0000000..419262d --- /dev/null +++ b/src/main/java/cn/isliu/core/service/CustomDimensionService.java @@ -0,0 +1,1083 @@ +package cn.isliu.core.service; + +import cn.isliu.core.client.FeishuApiClient; +import cn.isliu.core.client.FeishuClient; +import cn.isliu.core.pojo.ApiResponse; +import okhttp3.Request; +import okhttp3.RequestBody; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * 自定义维度服务 提供官方SDK未覆盖的行列操作API + */ +public class CustomDimensionService extends FeishuApiClient { + + /** + * 构造函数 + * + * @param feishuClient 飞书客户端 + */ + public CustomDimensionService(FeishuClient feishuClient) { + super(feishuClient); + } + + /** + * 批量操作行列 支持添加、插入等操作 支持处理多个请求,如果有请求失败则中断后续请求 + * + * @param spreadsheetToken 电子表格Token + * @param request 批量操作请求 + * @return 批量操作响应 + * @throws IOException 请求异常 + */ + public ApiResponse dimensionsBatchUpdate(String spreadsheetToken, + DimensionBatchUpdateRequest request) throws IOException { + List requests = request.getRequests(); + ApiResponse response = null; + + // 如果没有请求,返回空响应 + if (requests == null || requests.isEmpty()) { + ApiResponse emptyResponse = new ApiResponse(); + emptyResponse.setCode(400); + emptyResponse.setMsg("No dimension operations found"); + return emptyResponse; + } + + // 依次处理每个请求 + for (DimensionRequest dimensionRequest : requests) { + // 处理添加行列请求 + if (dimensionRequest.getAddDimension() != null) { + String url = BASE_URL + "/sheets/v2/spreadsheets/" + spreadsheetToken + "/dimension_range"; + + // 构建添加行列请求体 + RequestBody body = RequestBody.create( + gson.toJson(new AddDimensionRequestBody(dimensionRequest.getAddDimension())), JSON_MEDIA_TYPE); + + Request httpRequest = createAuthenticatedRequest(url, "POST", body).build(); + response = executeRequest(httpRequest, ApiResponse.class); + + // 如果请求失败,中断后续请求 + if (!response.success()) { + return response; + } + } + // 处理插入行列请求 + else if (dimensionRequest.getInsertDimension() != null) { + String url = BASE_URL + "/sheets/v2/spreadsheets/" + spreadsheetToken + "/insert_dimension_range"; + + // 构建插入行列请求体 + RequestBody body = + RequestBody.create(gson.toJson(new InsertDimensionRequestBody(dimensionRequest.getInsertDimension(), + dimensionRequest.getInheritStyle())), JSON_MEDIA_TYPE); + + Request httpRequest = createAuthenticatedRequest(url, "POST", body).build(); + response = executeRequest(httpRequest, ApiResponse.class); + + // 如果请求失败,中断后续请求 + if (!response.success()) { + return response; + } + } + // 处理更新行列请求 + else if (dimensionRequest.getUpdateDimension() != null) { + String url = BASE_URL + "/sheets/v2/spreadsheets/" + spreadsheetToken + "/dimension_range"; + + // 构建更新行列请求体 + RequestBody body = + RequestBody.create(gson.toJson(new UpdateDimensionRequestBody(dimensionRequest.getUpdateDimension(), + dimensionRequest.getDimensionProperties())), JSON_MEDIA_TYPE); + + // 使用PUT方法 + Request httpRequest = createAuthenticatedRequest(url, "PUT", body).build(); + response = executeRequest(httpRequest, ApiResponse.class); + + // 如果请求失败,中断后续请求 + if (!response.success()) { + return response; + } + } + // 处理删除行列请求 + else if (dimensionRequest.getDeleteDimension() != null) { + String url = BASE_URL + "/sheets/v2/spreadsheets/" + spreadsheetToken + "/dimension_range"; + + // 构建删除行列请求体 + RequestBody body = RequestBody.create( + gson.toJson(new DeleteDimensionRequestBody(dimensionRequest.getDeleteDimension())), + JSON_MEDIA_TYPE); + + // 使用DELETE方法 + Request httpRequest = createAuthenticatedRequest(url, "DELETE", body).build(); + response = executeRequest(httpRequest, ApiResponse.class); + + // 如果请求失败,中断后续请求 + if (!response.success()) { + return response; + } + } + } + + // 如果所有请求都成功处理,返回最后一个成功的响应 + // 如果没有处理任何请求(没有有效的操作类型),返回错误响应 + if (response == null) { + ApiResponse errorResponse = new ApiResponse(); + errorResponse.setCode(400); + errorResponse.setMsg("No valid dimension operation found"); + return errorResponse; + } + + return response; + } + + /** + * 批量操作行列请求 + */ + public static class DimensionBatchUpdateRequest { + private List requests; + + public DimensionBatchUpdateRequest() { + this.requests = new ArrayList<>(); + } + + public List getRequests() { + return requests; + } + + public void setRequests(List requests) { + this.requests = requests; + } + + /** + * 创建批量操作行列请求的构建器 + * + * @return 批量操作行列请求的构建器 + */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * 批量操作行列请求的构建器 + */ + public static class Builder { + private final DimensionBatchUpdateRequest request; + + public Builder() { + request = new DimensionBatchUpdateRequest(); + } + + /** + * 添加一个维度操作请求 + * + * @param dimensionRequest 维度操作请求,如添加行列或插入行列 + * @return 当前构建器 + */ + public Builder addRequest(DimensionRequest dimensionRequest) { + request.requests.add(dimensionRequest); + return this; + } + + /** + * 构建批量操作行列请求 + * + * @return 批量操作行列请求 + */ + public DimensionBatchUpdateRequest build() { + return request; + } + } + } + + /** + * 行列操作请求 + */ + public static class DimensionRequest { + private DimensionRange addDimension; + private InsertDimensionRange insertDimension; + private UpdateDimensionRange updateDimension; + private UpdateDimensionRange deleteDimension; + private DimensionProperties dimensionProperties; + private String inheritStyle; + + /** + * 获取添加行列的维度范围 + * + * @return 添加行列的维度范围 + */ + public DimensionRange getAddDimension() { + return addDimension; + } + + /** + * 设置添加行列的维度范围 + * + * @param addDimension 添加行列的维度范围 + */ + public void setAddDimension(DimensionRange addDimension) { + this.addDimension = addDimension; + } + + /** + * 获取插入行列的维度范围 + * + * @return 插入行列的维度范围 + */ + public InsertDimensionRange getInsertDimension() { + return insertDimension; + } + + /** + * 设置插入行列的维度范围 + * + * @param insertDimension 插入行列的维度范围 + */ + public void setInsertDimension(InsertDimensionRange insertDimension) { + this.insertDimension = insertDimension; + } + + /** + * 获取更新行列的维度范围 + * + * @return 更新行列的维度范围 + */ + public UpdateDimensionRange getUpdateDimension() { + return updateDimension; + } + + /** + * 设置更新行列的维度范围 + * + * @param updateDimension 更新行列的维度范围 + */ + public void setUpdateDimension(UpdateDimensionRange updateDimension) { + this.updateDimension = updateDimension; + } + + /** + * 获取删除行列的维度范围 + * + * @return 删除行列的维度范围 + */ + public UpdateDimensionRange getDeleteDimension() { + return deleteDimension; + } + + /** + * 设置删除行列的维度范围 + * + * @param deleteDimension 删除行列的维度范围 + */ + public void setDeleteDimension(UpdateDimensionRange deleteDimension) { + this.deleteDimension = deleteDimension; + } + + /** + * 获取维度属性 + * + * @return 维度属性 + */ + public DimensionProperties getDimensionProperties() { + return dimensionProperties; + } + + /** + * 设置维度属性 + * + * @param dimensionProperties 维度属性 + */ + public void setDimensionProperties(DimensionProperties dimensionProperties) { + this.dimensionProperties = dimensionProperties; + } + + /** + * 获取继承样式方式 + * + * @return 继承样式方式,可选值:BEFORE(继承前一行/列样式)、AFTER(继承后一行/列样式) + */ + public String getInheritStyle() { + return inheritStyle; + } + + /** + * 设置继承样式方式 + * + * @param inheritStyle 继承样式方式,可选值:BEFORE(继承前一行/列样式)、AFTER(继承后一行/列样式) + */ + public void setInheritStyle(String inheritStyle) { + this.inheritStyle = inheritStyle; + } + + /** + * 创建添加行列的维度请求构建器 用于在工作表末尾增加指定数量的行或列 + * + * @return 添加行列的构建器 + */ + public static AddDimensionBuilder addDimension() { + return new AddDimensionBuilder(); + } + + /** + * 创建插入行列的维度请求构建器 用于在工作表指定位置插入行或列 + * + * @return 插入行列的构建器 + */ + public static InsertDimensionBuilder insertDimension() { + return new InsertDimensionBuilder(); + } + + /** + * 创建更新行列的维度请求构建器 用于更新行或列的属性(如可见性、大小等) + * + * @return 更新行列的构建器 + */ + public static UpdateDimensionBuilder updateDimension() { + return new UpdateDimensionBuilder(); + } + + /** + * 创建删除行列的维度请求构建器 用于构建删除指定范围行列的请求 + * + * @return 删除行列的构建器 + */ + public static DeleteDimensionBuilder deleteDimension() { + return new DeleteDimensionBuilder(); + } + + /** + * 添加行列的构建器 用于构建添加行列的请求 + */ + public static class AddDimensionBuilder { + private final DimensionRequest request; + private final DimensionRange dimension; + + public AddDimensionBuilder() { + request = new DimensionRequest(); + dimension = new DimensionRange(); + request.setAddDimension(dimension); + } + + /** + * 设置工作表ID + * + * @param sheetId 工作表ID + * @return 当前构建器 + */ + public AddDimensionBuilder sheetId(String sheetId) { + dimension.sheetId = sheetId; + return this; + } + + /** + * 设置操作的维度类型 + * + * @param majorDimension 维度类型,可选值:ROWS(行)、COLUMNS(列) + * @return 当前构建器 + */ + public AddDimensionBuilder majorDimension(String majorDimension) { + dimension.majorDimension = majorDimension; + return this; + } + + /** + * 设置要添加的行数或列数 + * + * @param length 要添加的数量,取值范围(0,5000] + * @return 当前构建器 + */ + public AddDimensionBuilder length(Integer length) { + dimension.length = length; + return this; + } + + /** + * 构建添加行列请求 + * + * @return 维度操作请求 + */ + public DimensionRequest build() { + return request; + } + } + + /** + * 插入行列的构建器 用于构建在指定位置插入行列的请求 + */ + public static class InsertDimensionBuilder { + private final DimensionRequest request; + private final InsertDimensionRange dimension; + + public InsertDimensionBuilder() { + request = new DimensionRequest(); + dimension = new InsertDimensionRange(); + request.setInsertDimension(dimension); + } + + /** + * 设置工作表ID + * + * @param sheetId 工作表ID + * @return 当前构建器 + */ + public InsertDimensionBuilder sheetId(String sheetId) { + dimension.sheetId = sheetId; + return this; + } + + /** + * 设置操作的维度类型 + * + * @param majorDimension 维度类型,可选值:ROWS(行)、COLUMNS(列) + * @return 当前构建器 + */ + public InsertDimensionBuilder majorDimension(String majorDimension) { + dimension.majorDimension = majorDimension; + return this; + } + + /** + * 设置插入的起始位置 + * + * @param startIndex 起始位置索引,从0开始计数 + * @return 当前构建器 + */ + public InsertDimensionBuilder startIndex(Integer startIndex) { + dimension.startIndex = startIndex; + return this; + } + + /** + * 设置插入的结束位置 + * + * @param endIndex 结束位置索引,从0开始计数 + * @return 当前构建器 + */ + public InsertDimensionBuilder endIndex(Integer endIndex) { + dimension.endIndex = endIndex; + return this; + } + + /** + * 设置是否继承样式 + * + * @param inheritStyle 继承样式方式,可选值:BEFORE(继承前一行/列样式)、AFTER(继承后一行/列样式) + * @return 当前构建器 + */ + public InsertDimensionBuilder inheritStyle(String inheritStyle) { + request.inheritStyle = inheritStyle; + return this; + } + + /** + * 构建插入行列请求 + * + * @return 维度操作请求 + */ + public DimensionRequest build() { + return request; + } + } + + /** + * 更新行列的构建器 用于构建更新行列属性的请求 + */ + public static class UpdateDimensionBuilder { + private final DimensionRequest request; + private final UpdateDimensionRange dimension; + private final DimensionProperties properties; + + public UpdateDimensionBuilder() { + request = new DimensionRequest(); + dimension = new UpdateDimensionRange(); + properties = new DimensionProperties(); + request.setUpdateDimension(dimension); + request.setDimensionProperties(properties); + } + + /** + * 设置工作表ID + * + * @param sheetId 工作表ID + * @return 当前构建器 + */ + public UpdateDimensionBuilder sheetId(String sheetId) { + dimension.sheetId = sheetId; + return this; + } + + /** + * 设置操作的维度类型 + * + * @param majorDimension 维度类型,可选值:ROWS(行)、COLUMNS(列) + * @return 当前构建器 + */ + public UpdateDimensionBuilder majorDimension(String majorDimension) { + dimension.majorDimension = majorDimension; + return this; + } + + /** + * 设置更新的起始位置 + * + * @param startIndex 起始位置索引,从1开始计数 + * @return 当前构建器 + */ + public UpdateDimensionBuilder startIndex(Integer startIndex) { + dimension.startIndex = startIndex; + return this; + } + + /** + * 设置更新的结束位置 + * + * @param endIndex 结束位置索引,从1开始计数 + * @return 当前构建器 + */ + public UpdateDimensionBuilder endIndex(Integer endIndex) { + dimension.endIndex = endIndex; + return this; + } + + /** + * 设置是否显示行或列 + * + * @param visible true表示显示,false表示隐藏 + * @return 当前构建器 + */ + public UpdateDimensionBuilder visible(Boolean visible) { + properties.visible = visible; + return this; + } + + /** + * 设置行高或列宽 + * + * @param fixedSize 行高或列宽,单位为像素,0表示隐藏 + * @return 当前构建器 + */ + public UpdateDimensionBuilder fixedSize(Integer fixedSize) { + properties.fixedSize = fixedSize; + return this; + } + + /** + * 构建更新行列请求 + * + * @return 维度操作请求 + */ + public DimensionRequest build() { + return request; + } + } + + /** + * 删除行列的构建器 用于构建删除指定范围行列的请求 + */ + public static class DeleteDimensionBuilder { + private final DimensionRequest request; + private final UpdateDimensionRange dimension; + + public DeleteDimensionBuilder() { + request = new DimensionRequest(); + dimension = new UpdateDimensionRange(); + request.setDeleteDimension(dimension); + } + + /** + * 设置工作表ID + * + * @param sheetId 工作表ID + * @return 当前构建器 + */ + public DeleteDimensionBuilder sheetId(String sheetId) { + dimension.sheetId = sheetId; + return this; + } + + /** + * 设置操作的维度类型 + * + * @param majorDimension 维度类型,可选值:ROWS(行)、COLUMNS(列) + * @return 当前构建器 + */ + public DeleteDimensionBuilder majorDimension(String majorDimension) { + dimension.majorDimension = majorDimension; + return this; + } + + /** + * 设置删除的起始位置 + * + * @param startIndex 起始位置索引,从1开始计数 + * @return 当前构建器 + */ + public DeleteDimensionBuilder startIndex(Integer startIndex) { + dimension.startIndex = startIndex; + return this; + } + + /** + * 设置删除的结束位置 + * + * @param endIndex 结束位置索引,从1开始计数 + * @return 当前构建器 + */ + public DeleteDimensionBuilder endIndex(Integer endIndex) { + dimension.endIndex = endIndex; + return this; + } + + /** + * 构建删除行列请求 + * + * @return 维度操作请求 + */ + public DimensionRequest build() { + return request; + } + } + } + + /** + * 添加行列维度范围 + */ + public static class DimensionRange { + /** + * 电子表格工作表的ID 必填字段 + */ + private String sheetId; + + /** + * 更新的维度 必填字段 可选值: ROWS:行 COLUMNS:列 + */ + private String majorDimension; + + /** + * 要增加的行数或列数 必填字段 取值范围为(0,5000] + */ + private Integer length; + + /** + * 获取工作表ID + * + * @return 工作表ID + */ + public String getSheetId() { + return sheetId; + } + + /** + * 设置工作表ID + * + * @param sheetId 工作表ID + */ + public void setSheetId(String sheetId) { + this.sheetId = sheetId; + } + + /** + * 获取维度类型 + * + * @return 维度类型,ROWS表示行,COLUMNS表示列 + */ + public String getMajorDimension() { + return majorDimension; + } + + /** + * 设置维度类型 + * + * @param majorDimension 维度类型,ROWS表示行,COLUMNS表示列 + */ + public void setMajorDimension(String majorDimension) { + this.majorDimension = majorDimension; + } + + /** + * 获取要增加的行数或列数 + * + * @return 行数或列数 + */ + public Integer getLength() { + return length; + } + + /** + * 设置要增加的行数或列数 + * + * @param length 行数或列数,取值范围(0,5000] + */ + public void setLength(Integer length) { + this.length = length; + } + } + + /** + * 插入维度范围 + */ + public static class InsertDimensionRange { + /** + * 电子表格工作表的ID 必填字段 + */ + private String sheetId; + + /** + * 要更新的维度 必填字段 可选值: ROWS:行 COLUMNS:列 + */ + private String majorDimension; + + /** + * 插入的行或列的起始位置 必填字段 从0开始计数 若startIndex为3,则从第4行或列开始插入空行或列 包含第4行或列 + */ + private Integer startIndex; + + /** + * 插入的行或列结束的位置 必填字段 从0开始计数 若endIndex为7,则从第8行结束插入行 第8行不再插入空行 + */ + private Integer endIndex; + + /** + * 获取工作表ID + * + * @return 工作表ID + */ + public String getSheetId() { + return sheetId; + } + + /** + * 设置工作表ID + * + * @param sheetId 工作表ID + */ + public void setSheetId(String sheetId) { + this.sheetId = sheetId; + } + + /** + * 获取维度类型 + * + * @return 维度类型,ROWS表示行,COLUMNS表示列 + */ + public String getMajorDimension() { + return majorDimension; + } + + /** + * 设置维度类型 + * + * @param majorDimension 维度类型,ROWS表示行,COLUMNS表示列 + */ + public void setMajorDimension(String majorDimension) { + this.majorDimension = majorDimension; + } + + /** + * 获取插入的起始位置 + * + * @return 起始位置索引 + */ + public Integer getStartIndex() { + return startIndex; + } + + /** + * 设置插入的起始位置 + * + * @param startIndex 起始位置索引,从0开始计数 + */ + public void setStartIndex(Integer startIndex) { + this.startIndex = startIndex; + } + + /** + * 获取插入的结束位置 + * + * @return 结束位置索引 + */ + public Integer getEndIndex() { + return endIndex; + } + + /** + * 设置插入的结束位置 + * + * @param endIndex 结束位置索引,从0开始计数 + */ + public void setEndIndex(Integer endIndex) { + this.endIndex = endIndex; + } + } + + /** + * 更新维度范围 + */ + public static class UpdateDimensionRange { + /** + * 电子表格工作表的ID 必填字段 + */ + private String sheetId; + + /** + * 要更新的维度 必填字段 可选值: ROWS:行 COLUMNS:列 + */ + private String majorDimension; + + /** + * 要更新的行或列的起始位置 必填字段 从1开始计数 若startIndex为3,则从第3行或列开始更新属性 包含第3行或列 + */ + private Integer startIndex; + + /** + * 要更新的行或列结束的位置 必填字段 从1开始计数 若endIndex为7,则更新至第7行结束 包含第7行 + */ + private Integer endIndex; + + /** + * 获取工作表ID + * + * @return 工作表ID + */ + public String getSheetId() { + return sheetId; + } + + /** + * 设置工作表ID + * + * @param sheetId 工作表ID + */ + public void setSheetId(String sheetId) { + this.sheetId = sheetId; + } + + /** + * 获取维度类型 + * + * @return 维度类型,ROWS表示行,COLUMNS表示列 + */ + public String getMajorDimension() { + return majorDimension; + } + + /** + * 设置维度类型 + * + * @param majorDimension 维度类型,ROWS表示行,COLUMNS表示列 + */ + public void setMajorDimension(String majorDimension) { + this.majorDimension = majorDimension; + } + + /** + * 获取更新的起始位置 + * + * @return 起始位置索引 + */ + public Integer getStartIndex() { + return startIndex; + } + + /** + * 设置更新的起始位置 + * + * @param startIndex 起始位置索引,从1开始计数 + */ + public void setStartIndex(Integer startIndex) { + this.startIndex = startIndex; + } + + /** + * 获取更新的结束位置 + * + * @return 结束位置索引 + */ + public Integer getEndIndex() { + return endIndex; + } + + /** + * 设置更新的结束位置 + * + * @param endIndex 结束位置索引,从1开始计数 + */ + public void setEndIndex(Integer endIndex) { + this.endIndex = endIndex; + } + } + + /** + * 维度属性 + */ + public static class DimensionProperties { + /** + * 是否显示行或列 可选值: true:显示行或列 false:隐藏行或列 + */ + private Boolean visible; + + /** + * 行高或列宽 单位为像素 fixedSize为0时,等价于隐藏行或列 + */ + private Integer fixedSize; + + /** + * 获取是否显示 + * + * @return true表示显示,false表示隐藏 + */ + public Boolean getVisible() { + return visible; + } + + /** + * 设置是否显示 + * + * @param visible true表示显示,false表示隐藏 + */ + public void setVisible(Boolean visible) { + this.visible = visible; + } + + /** + * 获取行高或列宽 + * + * @return 行高或列宽,单位为像素 + */ + public Integer getFixedSize() { + return fixedSize; + } + + /** + * 设置行高或列宽 + * + * @param fixedSize 行高或列宽,单位为像素,0表示隐藏 + */ + public void setFixedSize(Integer fixedSize) { + this.fixedSize = fixedSize; + } + } + + /** + * 添加行列请求体(用于API请求) + */ + private static class AddDimensionRequestBody { + private final DimensionRange dimension; + + /** + * 构造函数 + * + * @param dimension 维度范围 + */ + public AddDimensionRequestBody(DimensionRange dimension) { + this.dimension = dimension; + } + + /** + * 获取维度范围 + * + * @return 维度范围 + */ + public DimensionRange getDimension() { + return dimension; + } + } + + /** + * 插入行列请求体(用于API请求) + */ + private static class InsertDimensionRequestBody { + private final InsertDimensionRange dimension; + private final String inheritStyle; + + /** + * 构造函数 + * + * @param dimension 维度范围 + * @param inheritStyle 继承样式方式 + */ + public InsertDimensionRequestBody(InsertDimensionRange dimension, String inheritStyle) { + this.dimension = dimension; + this.inheritStyle = inheritStyle; + } + + /** + * 获取维度范围 + * + * @return 维度范围 + */ + public InsertDimensionRange getDimension() { + return dimension; + } + + /** + * 获取继承样式方式 + * + * @return 继承样式方式 + */ + public String getInheritStyle() { + return inheritStyle; + } + } + + /** + * 更新行列请求体(用于API请求) + */ + private static class UpdateDimensionRequestBody { + private final UpdateDimensionRange dimension; + private final DimensionProperties dimensionProperties; + + /** + * 构造函数 + * + * @param dimension 维度范围 + * @param dimensionProperties 维度属性 + */ + public UpdateDimensionRequestBody(UpdateDimensionRange dimension, DimensionProperties dimensionProperties) { + this.dimension = dimension; + this.dimensionProperties = dimensionProperties; + } + + /** + * 获取维度范围 + * + * @return 维度范围 + */ + public UpdateDimensionRange getDimension() { + return dimension; + } + + /** + * 获取维度属性 + * + * @return 维度属性 + */ + public DimensionProperties getDimensionProperties() { + return dimensionProperties; + } + } + + /** + * 删除行列请求体(用于API请求) + */ + private static class DeleteDimensionRequestBody { + private final UpdateDimensionRange dimension; + + /** + * 构造函数 + * + * @param dimension 维度范围 + */ + public DeleteDimensionRequestBody(UpdateDimensionRange dimension) { + this.dimension = dimension; + } + + /** + * 获取维度范围 + * + * @return 维度范围 + */ + public UpdateDimensionRange getDimension() { + return dimension; + } + } +} \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/service/CustomProtectedDimensionService.java b/src/main/java/cn/isliu/core/service/CustomProtectedDimensionService.java new file mode 100644 index 0000000..d389cc9 --- /dev/null +++ b/src/main/java/cn/isliu/core/service/CustomProtectedDimensionService.java @@ -0,0 +1,453 @@ +package cn.isliu.core.service; + +import cn.isliu.core.client.FeishuApiClient; +import cn.isliu.core.client.FeishuClient; +import cn.isliu.core.pojo.ApiResponse; +import okhttp3.Request; +import okhttp3.RequestBody; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * 自定义保护范围服务 提供保护行列的API + */ +public class CustomProtectedDimensionService extends FeishuApiClient { + + /** + * 构造函数 + * + * @param feishuClient 飞书客户端 + */ + public CustomProtectedDimensionService(FeishuClient feishuClient) { + super(feishuClient); + } + + /** + * 批量操作保护范围 支持添加保护范围 支持处理多个请求,如果有请求失败则中断后续请求 + * + * @param spreadsheetToken 电子表格Token + * @param request 批量操作请求 + * @return 批量操作响应 + * @throws IOException 请求异常 + */ + public ApiResponse protectedDimensionBatchUpdate(String spreadsheetToken, + ProtectedDimensionBatchUpdateRequest request) throws IOException { + List requests = request.getRequests(); + ApiResponse response = null; + + // 如果没有请求,返回空响应 + if (requests == null || requests.isEmpty()) { + ApiResponse emptyResponse = new ApiResponse(); + emptyResponse.setCode(400); + emptyResponse.setMsg("No protected dimension operations found"); + return emptyResponse; + } + + // 依次处理每个请求 + for (ProtectedDimensionRequest protectedDimensionRequest : requests) { + // 处理添加保护范围请求 + if (protectedDimensionRequest.getAddProtectedDimension() != null) { + String url = BASE_URL + "/sheets/v2/spreadsheets/" + spreadsheetToken + "/protected_dimension"; + + // 构建添加保护范围请求体 + RequestBody body = RequestBody.create( + gson.toJson( + new AddProtectedDimensionRequestBody(protectedDimensionRequest.getAddProtectedDimension())), + JSON_MEDIA_TYPE); + + Request httpRequest = createAuthenticatedRequest(url, "POST", body).build(); + response = executeRequest(httpRequest, ApiResponse.class); + + // 如果请求失败,中断后续请求 + if (!response.success()) { + return response; + } + } + // 这里可以添加其他保护范围操作类型 + } + + // 如果所有请求都成功处理,返回最后一个成功的响应 + // 如果没有处理任何请求(没有有效的操作类型),返回错误响应 + if (response == null) { + ApiResponse errorResponse = new ApiResponse(); + errorResponse.setCode(400); + errorResponse.setMsg("No valid protected dimension operation found"); + return errorResponse; + } + + return response; + } + + /** + * 批量操作保护范围请求 + */ + public static class ProtectedDimensionBatchUpdateRequest { + private List requests; + + public ProtectedDimensionBatchUpdateRequest() { + this.requests = new ArrayList<>(); + } + + public List getRequests() { + return requests; + } + + public void setRequests(List requests) { + this.requests = requests; + } + + /** + * 创建批量操作保护范围请求的构建器 + * + * @return 批量操作保护范围请求的构建器 + */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * 批量操作保护范围请求的构建器 + */ + public static class Builder { + private final ProtectedDimensionBatchUpdateRequest request; + + public Builder() { + request = new ProtectedDimensionBatchUpdateRequest(); + } + + /** + * 添加一个保护范围操作请求 + * + * @param protectedDimensionRequest 保护范围操作请求 + * @return 当前构建器 + */ + public Builder addRequest(ProtectedDimensionRequest protectedDimensionRequest) { + request.requests.add(protectedDimensionRequest); + return this; + } + + /** + * 构建批量操作保护范围请求 + * + * @return 批量操作保护范围请求 + */ + public ProtectedDimensionBatchUpdateRequest build() { + return request; + } + } + } + + /** + * 保护范围操作请求 + */ + public static class ProtectedDimensionRequest { + private List addProtectedDimension; + + /** + * 获取添加保护范围的信息 + * + * @return 添加保护范围的信息 + */ + public List getAddProtectedDimension() { + return addProtectedDimension; + } + + /** + * 设置添加保护范围的信息 + * + * @param addProtectedDimension 添加保护范围的信息 + */ + public void setAddProtectedDimension(List addProtectedDimension) { + this.addProtectedDimension = addProtectedDimension; + } + + /** + * 创建添加保护范围构建器 + * + * @return 添加保护范围构建器 + */ + public static AddProtectedDimensionBuilder addProtectedDimension() { + return new AddProtectedDimensionBuilder(); + } + + /** + * 添加保护范围构建器 + */ + public static class AddProtectedDimensionBuilder { + private final ProtectedDimensionRequest request; + private final AddProtectedDimensionRange protectedDimension; + + public AddProtectedDimensionBuilder() { + request = new ProtectedDimensionRequest(); + protectedDimension = new AddProtectedDimensionRange(); + + List list = new ArrayList<>(); + list.add(protectedDimension); + request.setAddProtectedDimension(list); + } + + /** + * 设置工作表ID + * + * @param sheetId 工作表ID + * @return 当前构建器 + */ + public AddProtectedDimensionBuilder sheetId(String sheetId) { + protectedDimension.getDimension().setSheetId(sheetId); + return this; + } + + /** + * 设置维度方向 + * + * @param majorDimension 维度方向,可选值:ROWS(行)、COLUMNS(列) + * @return 当前构建器 + */ + public AddProtectedDimensionBuilder majorDimension(String majorDimension) { + protectedDimension.getDimension().setMajorDimension(majorDimension); + return this; + } + + /** + * 设置开始索引 + * + * @param startIndex 开始索引,从1开始计数 + * @return 当前构建器 + */ + public AddProtectedDimensionBuilder startIndex(Integer startIndex) { + protectedDimension.getDimension().setStartIndex(startIndex); + return this; + } + + /** + * 设置结束索引 + * + * @param endIndex 结束索引,从1开始计数 + * @return 当前构建器 + */ + public AddProtectedDimensionBuilder endIndex(Integer endIndex) { + protectedDimension.getDimension().setEndIndex(endIndex); + return this; + } + + /** + * 添加一个允许编辑的用户ID + * + * @param userId 用户ID + * @return 当前构建器 + */ + public AddProtectedDimensionBuilder addUser(String userId) { + protectedDimension.getUsers().add(userId); + return this; + } + + /** + * 设置多个允许编辑的用户ID + * + * @param userIds 用户ID列表 + * @return 当前构建器 + */ + public AddProtectedDimensionBuilder users(List userIds) { + protectedDimension.setUsers(userIds); + return this; + } + + /** + * 设置保护范围的备注信息 + * + * @param lockInfo 备注信息 + * @return 当前构建器 + */ + public AddProtectedDimensionBuilder lockInfo(String lockInfo) { + protectedDimension.setLockInfo(lockInfo); + return this; + } + + /** + * 构建保护范围请求 + * + * @return 保护范围请求 + */ + public ProtectedDimensionRequest build() { + return request; + } + } + } + + /** + * 添加保护范围信息 + */ + public static class AddProtectedDimensionRange { + private DimensionRange dimension; + private List users; + private String lockInfo; + + public AddProtectedDimensionRange() { + this.dimension = new DimensionRange(); + this.users = new ArrayList<>(); + } + + /** + * 获取维度范围 + * + * @return 维度范围 + */ + public DimensionRange getDimension() { + return dimension; + } + + /** + * 设置维度范围 + * + * @param dimension 维度范围 + */ + public void setDimension(DimensionRange dimension) { + this.dimension = dimension; + } + + /** + * 获取允许编辑的用户ID列表 + * + * @return 用户ID列表 + */ + public List getUsers() { + return users; + } + + /** + * 设置允许编辑的用户ID列表 + * + * @param users 用户ID列表 + */ + public void setUsers(List users) { + this.users = users; + } + + /** + * 获取保护范围的备注信息 + * + * @return 备注信息 + */ + public String getLockInfo() { + return lockInfo; + } + + /** + * 设置保护范围的备注信息 + * + * @param lockInfo 备注信息 + */ + public void setLockInfo(String lockInfo) { + this.lockInfo = lockInfo; + } + } + + /** + * 维度范围 + */ + public static class DimensionRange { + private String sheetId; + private String majorDimension; + private Integer startIndex; + private Integer endIndex; + + /** + * 获取工作表ID + * + * @return 工作表ID + */ + public String getSheetId() { + return sheetId; + } + + /** + * 设置工作表ID + * + * @param sheetId 工作表ID + */ + public void setSheetId(String sheetId) { + this.sheetId = sheetId; + } + + /** + * 获取维度方向 + * + * @return 维度方向,ROWS(行)或COLUMNS(列) + */ + public String getMajorDimension() { + return majorDimension; + } + + /** + * 设置维度方向 + * + * @param majorDimension 维度方向,ROWS(行)或COLUMNS(列) + */ + public void setMajorDimension(String majorDimension) { + this.majorDimension = majorDimension; + } + + /** + * 获取开始索引 + * + * @return 开始索引,从1开始计数 + */ + public Integer getStartIndex() { + return startIndex; + } + + /** + * 设置开始索引 + * + * @param startIndex 开始索引,从1开始计数 + */ + public void setStartIndex(Integer startIndex) { + this.startIndex = startIndex; + } + + /** + * 获取结束索引 + * + * @return 结束索引,从1开始计数 + */ + public Integer getEndIndex() { + return endIndex; + } + + /** + * 设置结束索引 + * + * @param endIndex 结束索引,从1开始计数 + */ + public void setEndIndex(Integer endIndex) { + this.endIndex = endIndex; + } + } + + /** + * 添加保护范围请求体 + */ + private static class AddProtectedDimensionRequestBody { + private final List addProtectedDimension; + + /** + * 构造函数 + * + * @param addProtectedDimension 添加保护范围信息列表 + */ + public AddProtectedDimensionRequestBody(List addProtectedDimension) { + this.addProtectedDimension = addProtectedDimension; + } + + /** + * 获取添加保护范围信息列表 + * + * @return 添加保护范围信息列表 + */ + public List getAddProtectedDimension() { + return addProtectedDimension; + } + } +} \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/service/CustomSheetService.java b/src/main/java/cn/isliu/core/service/CustomSheetService.java new file mode 100644 index 0000000..33d8c8c --- /dev/null +++ b/src/main/java/cn/isliu/core/service/CustomSheetService.java @@ -0,0 +1,870 @@ +package cn.isliu.core.service; + +import cn.isliu.core.client.FeishuApiClient; +import cn.isliu.core.client.FeishuClient; +import cn.isliu.core.pojo.ApiResponse; +import okhttp3.Request; +import okhttp3.RequestBody; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * 自定义表格服务 提供官方SDK未覆盖的表格API + */ +public class CustomSheetService extends FeishuApiClient { + + /** + * 构造函数 + * + * @param feishuClient 飞书客户端 + */ + public CustomSheetService(FeishuClient feishuClient) { + super(feishuClient); + } + + /** + * 批量更新工作表 支持添加、复制、删除等操作 + * + * @param spreadsheetToken 电子表格Token + * @param request 批量更新请求 + * @return 批量更新响应 + * @throws IOException 请求异常 + */ + public ApiResponse sheetsBatchUpdate(String spreadsheetToken, SheetBatchUpdateRequest request) + throws IOException { + List requests = request.getRequests(); + // 更新工作表特殊字段兼容 + String userIdType = null; + for (SheetRequest sheetRequest : requests) { + UpdateSheetRequest updateSheet = sheetRequest.getUpdateSheet(); + if (updateSheet != null) { + SheetPropertiesUpdate properties = updateSheet.getProperties(); + userIdType = properties.getUserIdType(); + break; + } + } + String url = BASE_URL + "/sheets/v2/spreadsheets/" + spreadsheetToken + "/sheets_batch_update"; + if (userIdType != null && !userIdType.isEmpty()) { + url += "?user_id_type=" + userIdType; + } + + RequestBody body = RequestBody.create(gson.toJson(request), JSON_MEDIA_TYPE); + + Request httpRequest = createAuthenticatedRequest(url, "POST", body).build(); + return executeRequest(httpRequest, ApiResponse.class); + } + + /** + * 批量更新工作表请求 + */ + public static class SheetBatchUpdateRequest { + /** + * 支持增加、复制、删除和更新工作表 一次请求可以同时进行多个操作 + */ + private List requests; + + public SheetBatchUpdateRequest() { + this.requests = new ArrayList<>(); + } + + public List getRequests() { + return requests; + } + + public void setRequests(List requests) { + this.requests = requests; + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static class Builder { + private final SheetBatchUpdateRequest request; + + public Builder() { + request = new SheetBatchUpdateRequest(); + } + + public Builder addRequest(SheetRequest sheetRequest) { + request.requests.add(sheetRequest); + return this; + } + + public SheetBatchUpdateRequest build() { + return request; + } + } + } + + /** + * 工作表请求 + */ + public static class SheetRequest { + /** + * 增加工作表操作 + */ + private AddSheetRequest addSheet; + + /** + * 复制工作表操作 + */ + private CopySheetRequest copySheet; + + /** + * 删除工作表操作 + */ + private DeleteSheetRequest deleteSheet; + + /** + * 更新工作表操作 + */ + private UpdateSheetRequest updateSheet; + + public AddSheetRequest getAddSheet() { + return addSheet; + } + + public void setAddSheet(AddSheetRequest addSheet) { + this.addSheet = addSheet; + } + + public CopySheetRequest getCopySheet() { + return copySheet; + } + + public void setCopySheet(CopySheetRequest copySheet) { + this.copySheet = copySheet; + } + + public DeleteSheetRequest getDeleteSheet() { + return deleteSheet; + } + + public void setDeleteSheet(DeleteSheetRequest deleteSheet) { + this.deleteSheet = deleteSheet; + } + + public UpdateSheetRequest getUpdateSheet() { + return updateSheet; + } + + public void setUpdateSheet(UpdateSheetRequest updateSheet) { + this.updateSheet = updateSheet; + } + + /** + * 创建添加工作表的请求构建器 用于在电子表格中添加新的工作表 + * + * @return 添加工作表的构建器 + */ + public static AddSheetBuilder addSheet() { + return new AddSheetBuilder(); + } + + /** + * 创建复制工作表的请求构建器 用于复制已有的工作表到同一电子表格中 + * + * @return 复制工作表的构建器 + */ + public static CopySheetBuilder copySheet() { + return new CopySheetBuilder(); + } + + /** + * 创建删除工作表的请求构建器 用于删除电子表格中的指定工作表 + * + * @return 删除工作表的构建器 + */ + public static DeleteSheetBuilder deleteSheet() { + return new DeleteSheetBuilder(); + } + + /** + * 创建更新工作表的请求构建器 用于更新工作表的标题、位置、显示状态、冻结行列、保护设置等属性 + * + * @return 更新工作表的构建器 + */ + public static UpdateSheetBuilder updateSheet() { + return new UpdateSheetBuilder(); + } + + /** + * 添加工作表的构建器 用于构建添加新工作表的请求 + */ + public static class AddSheetBuilder { + private final SheetRequest request; + private final AddSheetRequest addSheet; + + public AddSheetBuilder() { + request = new SheetRequest(); + addSheet = new AddSheetRequest(); + request.setAddSheet(addSheet); + } + + /** + * 获取或初始化properties对象 + * + * @return properties对象 + */ + private SheetProperties getProperties() { + if (addSheet.properties == null) { + addSheet.properties = new SheetProperties(); + } + return addSheet.properties; + } + + /** + * 设置工作表标题 + * + * @param title 工作表标题,不能包含特殊字符:/ \ ? * [ ] : + * @return 当前构建器 + */ + public AddSheetBuilder title(String title) { + getProperties().title = title; + return this; + } + + /** + * 设置工作表在电子表格中的位置索引 + * + * @param index 位置索引,从0开始计数,不填默认在第0位置添加 + * @return 当前构建器 + */ + public AddSheetBuilder index(Integer index) { + getProperties().index = index; + return this; + } + + /** + * 构建添加工作表请求 + * + * @return 工作表操作请求 + */ + public SheetRequest build() { + return request; + } + } + + /** + * 复制工作表的构建器 用于构建复制现有工作表的请求 + */ + public static class CopySheetBuilder { + private final SheetRequest request; + private final CopySheetRequest copySheet; + + public CopySheetBuilder() { + request = new SheetRequest(); + copySheet = new CopySheetRequest(); + request.setCopySheet(copySheet); + } + + /** + * 获取或初始化source对象 + * + * @return source对象 + */ + private SheetSource getSource() { + if (copySheet.source == null) { + copySheet.source = new SheetSource(); + } + return copySheet.source; + } + + /** + * 获取或初始化destination对象 + * + * @return destination对象 + */ + private SheetDestination getDestination() { + if (copySheet.destination == null) { + copySheet.destination = new SheetDestination(); + } + return copySheet.destination; + } + + /** + * 设置源工作表ID + * + * @param sheetId 要复制的源工作表ID + * @return 当前构建器 + */ + public CopySheetBuilder sourceSheetId(String sheetId) { + getSource().sheetId = sheetId; + return this; + } + + /** + * 设置目标工作表标题 + * + * @param title 复制后的工作表标题,不填则默认为"源工作表名称(副本_源工作表的index值)" + * @return 当前构建器 + */ + public CopySheetBuilder destinationTitle(String title) { + getDestination().title = title; + return this; + } + + /** + * 构建复制工作表请求 + * + * @return 工作表操作请求 + */ + public SheetRequest build() { + return request; + } + } + + /** + * 删除工作表的构建器 用于构建删除工作表的请求 + */ + public static class DeleteSheetBuilder { + private final SheetRequest request; + private final DeleteSheetRequest deleteSheet; + + public DeleteSheetBuilder() { + request = new SheetRequest(); + deleteSheet = new DeleteSheetRequest(); + request.setDeleteSheet(deleteSheet); + } + + /** + * 设置要删除的工作表ID + * + * @param sheetId 要删除的工作表ID + * @return 当前构建器 + */ + public DeleteSheetBuilder sheetId(String sheetId) { + deleteSheet.sheetId = sheetId; + return this; + } + + /** + * 构建删除工作表请求 + * + * @return 工作表操作请求 + */ + public SheetRequest build() { + return request; + } + } + + /** + * 更新工作表的构建器 用于构建更新工作表属性的请求 + */ + public static class UpdateSheetBuilder { + private final SheetRequest request; + private final UpdateSheetRequest updateSheet; + + public UpdateSheetBuilder() { + request = new SheetRequest(); + updateSheet = new UpdateSheetRequest(); + request.setUpdateSheet(updateSheet); + } + + /** + * 获取或初始化properties对象 + * + * @return properties对象 + */ + private SheetPropertiesUpdate getProperties() { + if (updateSheet.properties == null) { + updateSheet.properties = new SheetPropertiesUpdate(); + } + return updateSheet.properties; + } + + /** + * 设置要更新的工作表ID + * + * @param sheetId 工作表ID + * @return 当前构建器 + */ + public UpdateSheetBuilder sheetId(String sheetId) { + getProperties().sheetId = sheetId; + return this; + } + + /** + * 设置工作表标题 + * + * @param title 工作表标题,不能包含特殊字符:/ \ ? * [ ] : + * @return 当前构建器 + */ + public UpdateSheetBuilder title(String title) { + getProperties().title = title; + return this; + } + + /** + * 设置工作表在电子表格中的位置索引 + * + * @param index 位置索引,从0开始计数 + * @return 当前构建器 + */ + public UpdateSheetBuilder index(Integer index) { + getProperties().index = index; + return this; + } + + /** + * 设置工作表是否隐藏 + * + * @param hidden true表示隐藏,false表示显示 + * @return 当前构建器 + */ + public UpdateSheetBuilder hidden(Boolean hidden) { + getProperties().hidden = hidden; + return this; + } + + /** + * 设置冻结行数 + * + * @param frozenRowCount 冻结的行数,0表示取消冻结 + * @return 当前构建器 + */ + public UpdateSheetBuilder frozenRowCount(Integer frozenRowCount) { + getProperties().frozenRowCount = frozenRowCount; + return this; + } + + /** + * 设置冻结列数 + * + * @param frozenColCount 冻结的列数,0表示取消冻结 + * @return 当前构建器 + */ + public UpdateSheetBuilder frozenColCount(Integer frozenColCount) { + getProperties().frozenColCount = frozenColCount; + return this; + } + + /** + * 设置用户ID类型 + * + * @param userIdType 用户ID类型,建议使用open_id或union_id,可选值:open_id、union_id + * @return 当前构建器 + */ + public UpdateSheetBuilder userIdType(String userIdType) { + getProperties().userIdType = userIdType; + return this; + } + + /** + * 设置工作表保护 + * + * @param lock 是否要保护该工作表,可选值:LOCK(保护)、UNLOCK(取消保护) + * @param lockInfo 保护工作表的备注信息 + * @param userIDs 有编辑权限的用户ID列表 + * @return 当前构建器 + */ + public UpdateSheetBuilder protect(String lock, String lockInfo, List userIDs) { + SheetPropertiesUpdate properties = getProperties(); + if (properties.protect == null) { + properties.protect = new SheetProtect(); + } + properties.protect.lock = lock; + properties.protect.lockInfo = lockInfo; + properties.protect.userIDs = userIDs; + return this; + } + + /** + * 构建更新工作表请求 + * + * @return 工作表操作请求 + */ + public SheetRequest build() { + return request; + } + } + } + + /** + * 添加工作表请求 + */ + public static class AddSheetRequest { + /** + * 工作表属性 + */ + private SheetProperties properties; + + public SheetProperties getProperties() { + return properties; + } + + public void setProperties(SheetProperties properties) { + this.properties = properties; + } + } + + /** + * 工作表属性 + */ + public static class SheetProperties { + /** + * 新增工作表的标题 必填字段 + */ + private String title; + + /** + * 新增工作表的位置 不填默认在工作表的第0索引位置增加工作表 + */ + private Integer index; + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Integer getIndex() { + return index; + } + + public void setIndex(Integer index) { + this.index = index; + } + } + + /** + * 复制工作表请求 + */ + public static class CopySheetRequest { + /** + * 需要复制的工作表资源 + */ + private SheetSource source; + + /** + * 新工作表的属性 + */ + private SheetDestination destination; + + public SheetSource getSource() { + return source; + } + + public void setSource(SheetSource source) { + this.source = source; + } + + public SheetDestination getDestination() { + return destination; + } + + public void setDestination(SheetDestination destination) { + this.destination = destination; + } + } + + /** + * 工作表源 + */ + public static class SheetSource { + /** + * 源工作表的ID 必填字段 + */ + private String sheetId; + + public String getSheetId() { + return sheetId; + } + + public void setSheetId(String sheetId) { + this.sheetId = sheetId; + } + } + + /** + * 工作表目标 + */ + public static class SheetDestination { + /** + * 新工作表名称 不填默认为"源工作表名称"+"(副本_源工作表的index值)",如"Sheet1(副本_0)" + */ + private String title; + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + /** + * 删除工作表请求 + */ + public static class DeleteSheetRequest { + /** + * 要删除的工作表的ID 必填字段 + */ + private String sheetId; + + public String getSheetId() { + return sheetId; + } + + public void setSheetId(String sheetId) { + this.sheetId = sheetId; + } + } + + /** + * 更新工作表请求 + */ + public static class UpdateSheetRequest { + /** + * 工作表属性 + */ + private SheetPropertiesUpdate properties; + + public SheetPropertiesUpdate getProperties() { + return properties; + } + + public void setProperties(SheetPropertiesUpdate properties) { + this.properties = properties; + } + } + + /** + * 工作表属性更新 + */ + public static class SheetPropertiesUpdate { + /** + * 要更新的工作表的ID 必填字段 + */ + private String sheetId; + + /** + * 工作表的标题 更新的标题需符合以下规则: 长度不超过100个字符 不包含这些特殊字符:/ \ ? * [ ] : + */ + private String title; + + /** + * 工作表的位置 从0开始计数 + */ + private Integer index; + + /** + * 是否要隐藏表格 默认值为false + */ + private Boolean hidden; + + /** + * 要冻结至指定行的行索引 若填3,表示从第一行冻结至第三行 小于或等于工作表的最大行数,0表示取消冻结行 + */ + private Integer frozenRowCount; + + /** + * 要冻结至指定列的列索引 若填3,表示从第一列冻结至第三列 小于等于工作表的最大列数,0表示取消冻结列 + */ + private Integer frozenColCount; + + /** + * 工作表保护设置 + */ + private SheetProtect protect; + + /** + * 用户ID类型 影响protect.userIDs字段的ID类型 用户 ID 类型。默认为 lark_id,建议选择 open_id 或 union_id。了解更多,参考用户身份概述。可选值: + * open_id:标识一个用户在某个应用中的身份。同一个用户在不同应用中的 Open ID 不同。 union_id:标识一个用户在某个应用开发商下的身份。同一用户在同一开发商下的应用中的 Union ID + * 是相同的,在不同开发商下的应用中的 Union ID 是不同的。通过 Union ID,应用开发商可以把同个用户在多个应用中的身份关联起来。 + */ + private String userIdType; + + public String getSheetId() { + return sheetId; + } + + public void setSheetId(String sheetId) { + this.sheetId = sheetId; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Integer getIndex() { + return index; + } + + public void setIndex(Integer index) { + this.index = index; + } + + public Boolean getHidden() { + return hidden; + } + + public void setHidden(Boolean hidden) { + this.hidden = hidden; + } + + public Integer getFrozenRowCount() { + return frozenRowCount; + } + + public void setFrozenRowCount(Integer frozenRowCount) { + this.frozenRowCount = frozenRowCount; + } + + public Integer getFrozenColCount() { + return frozenColCount; + } + + public void setFrozenColCount(Integer frozenColCount) { + this.frozenColCount = frozenColCount; + } + + public SheetProtect getProtect() { + return protect; + } + + public void setProtect(SheetProtect protect) { + this.protect = protect; + } + + public String getUserIdType() { + return userIdType; + } + + public void setUserIdType(String userIdType) { + this.userIdType = userIdType; + } + } + + /** + * 工作表保护设置 + */ + public static class SheetProtect { + /** + * 是否要保护该工作表 必填字段 可选值: LOCK:保护 UNLOCK:取消保护 + */ + private String lock; + + /** + * 保护工作表的备注信息 + */ + private String lockInfo; + + /** + * 添加除操作用户与所有者外其他用户的ID 为其开通保护范围的编辑权限 ID类型由查询参数user_id_type决定 user_id_type不为空时,该字段生效 + */ + private List userIDs; + + public String getLock() { + return lock; + } + + public void setLock(String lock) { + this.lock = lock; + } + + public String getLockInfo() { + return lockInfo; + } + + public void setLockInfo(String lockInfo) { + this.lockInfo = lockInfo; + } + + public List getUserIDs() { + return userIDs; + } + + public void setUserIDs(List userIDs) { + this.userIDs = userIDs; + } + } + + /** + * 工作表 + */ + public static class Sheet { + private String sheetId; + private String title; + private Integer index; + private Integer rowCount; + private Integer columnCount; + private Boolean hidden; + private Integer frozenRowCount; + private Integer frozenColCount; + + public String getSheetId() { + return sheetId; + } + + public void setSheetId(String sheetId) { + this.sheetId = sheetId; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Integer getIndex() { + return index; + } + + public void setIndex(Integer index) { + this.index = index; + } + + public Integer getRowCount() { + return rowCount; + } + + public void setRowCount(Integer rowCount) { + this.rowCount = rowCount; + } + + public Integer getColumnCount() { + return columnCount; + } + + public void setColumnCount(Integer columnCount) { + this.columnCount = columnCount; + } + + public Boolean getHidden() { + return hidden; + } + + public void setHidden(Boolean hidden) { + this.hidden = hidden; + } + + public Integer getFrozenRowCount() { + return frozenRowCount; + } + + public void setFrozenRowCount(Integer frozenRowCount) { + this.frozenRowCount = frozenRowCount; + } + + public Integer getFrozenColCount() { + return frozenColCount; + } + + public void setFrozenColCount(Integer frozenColCount) { + this.frozenColCount = frozenColCount; + } + } +} \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/service/CustomValueService.java b/src/main/java/cn/isliu/core/service/CustomValueService.java new file mode 100644 index 0000000..5f9ea05 --- /dev/null +++ b/src/main/java/cn/isliu/core/service/CustomValueService.java @@ -0,0 +1,1672 @@ +package cn.isliu.core.service; + +import cn.isliu.core.client.FeishuApiClient; +import cn.isliu.core.client.FeishuClient; +import cn.isliu.core.pojo.ApiResponse; +import okhttp3.Request; +import okhttp3.RequestBody; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * 自定义数据值服务 提供官方SDK未覆盖的数据操作API + */ +public class CustomValueService extends FeishuApiClient { + + /** + * 构造函数 + * + * @param feishuClient 飞书客户端 + */ + public CustomValueService(FeishuClient feishuClient) { + super(feishuClient); + } + + /** + * 批量操作数据值 支持在指定范围前插入数据、在指定范围后追加数据等操作 + * + * @param spreadsheetToken 电子表格Token + * @param request 批量操作请求 + * @return 批量操作响应 + * @throws IOException 请求异常 + */ + public ApiResponse valueBatchUpdate(String spreadsheetToken, ValueBatchUpdateRequest request) throws IOException { + List requests = request.getRequests(); + ApiResponse response = null; + + // 如果没有请求,返回空响应 + if (requests == null || requests.isEmpty()) { + ApiResponse emptyResponse = new ApiResponse(); + emptyResponse.setCode(400); + emptyResponse.setMsg("No value operations found"); + return emptyResponse; + } + + // 依次处理每个请求 + for (ValueRequest valueRequest : requests) { + // 处理在指定范围前插入数据请求 + if (valueRequest.getPrependValues() != null) { + String url = BASE_URL + "/sheets/v2/spreadsheets/" + spreadsheetToken + "/values_prepend"; + + RequestBody body = RequestBody.create(gson.toJson(valueRequest.getPrependValues()), JSON_MEDIA_TYPE); + Request httpRequest = createAuthenticatedRequest(url, "POST", body).build(); + response = executeRequest(httpRequest, ApiResponse.class); + + // 如果请求失败,中断后续请求 + if (!response.success()) { + return response; + } + } + // 处理在指定范围后追加数据请求 + else if (valueRequest.getAppendValues() != null) { + String url = BASE_URL + "/sheets/v2/spreadsheets/" + spreadsheetToken + "/values_append"; + + // 添加insertDataOption参数 + String insertDataOption = valueRequest.getAppendValues().getInsertDataOption(); + if (insertDataOption != null && !insertDataOption.isEmpty()) { + url += "?insertDataOption=" + insertDataOption; + } + + RequestBody body = RequestBody.create(gson.toJson(valueRequest.getAppendValues()), JSON_MEDIA_TYPE); + Request httpRequest = createAuthenticatedRequest(url, "POST", body).build(); + response = executeRequest(httpRequest, ApiResponse.class); + + // 如果请求失败,中断后续请求 + if (!response.success()) { + return response; + } + } + // 处理写入图片请求 + else if (valueRequest.getImageValues() != null) { + String url = BASE_URL + "/sheets/v2/spreadsheets/" + spreadsheetToken + "/values_image"; + + RequestBody body = RequestBody.create(gson.toJson(valueRequest.getImageValues()), JSON_MEDIA_TYPE); + Request httpRequest = createAuthenticatedRequest(url, "POST", body).build(); + response = executeRequest(httpRequest, ApiResponse.class); + + // 如果请求失败,中断后续请求 + if (!response.success()) { + return response; + } + } + // 处理获取单个范围请求 + else if (valueRequest.getGetValues() != null) { + ValueGetRequest getValues = valueRequest.getGetValues(); + String baseUrl = + BASE_URL + "/sheets/v2/spreadsheets/" + spreadsheetToken + "/values/" + getValues.getRange(); + + // 构建查询参数 + StringBuilder urlBuilder = new StringBuilder(baseUrl); + boolean hasParam = false; + + // 添加valueRenderOption参数 + if (getValues.getValueRenderOption() != null && !getValues.getValueRenderOption().isEmpty()) { + urlBuilder.append(hasParam ? "&" : "?").append("valueRenderOption=") + .append(URLEncoder.encode(getValues.getValueRenderOption(), StandardCharsets.UTF_8.toString())); + hasParam = true; + } + + // 添加dateTimeRenderOption参数 + if (getValues.getDateTimeRenderOption() != null && !getValues.getDateTimeRenderOption().isEmpty()) { + urlBuilder.append(hasParam ? "&" : "?").append("dateTimeRenderOption=").append( + URLEncoder.encode(getValues.getDateTimeRenderOption(), StandardCharsets.UTF_8.toString())); + hasParam = true; + } + + // 添加user_id_type参数 + if (getValues.getUserIdType() != null && !getValues.getUserIdType().isEmpty()) { + urlBuilder.append(hasParam ? "&" : "?").append("user_id_type=") + .append(URLEncoder.encode(getValues.getUserIdType(), StandardCharsets.UTF_8.toString())); + } + + String url = urlBuilder.toString(); + Request httpRequest = createAuthenticatedRequest(url, "GET", null).build(); + response = executeRequest(httpRequest, ApiResponse.class); + + // 如果请求失败,中断后续请求 + if (!response.success()) { + return response; + } + } + // 处理批量获取多个范围请求 + else if (valueRequest.getBatchGetValues() != null) { + ValueBatchGetRequest batchGetValues = valueRequest.getBatchGetValues(); + + // 检查ranges是否为空 + if (batchGetValues.getRanges() == null || batchGetValues.getRanges().isEmpty()) { + ApiResponse errorResponse = new ApiResponse(); + errorResponse.setCode(400); + errorResponse.setMsg("Ranges cannot be empty for batch get values"); + return errorResponse; + } + + // 构建ranges参数,使用逗号分隔 + StringBuilder rangesBuilder = new StringBuilder(); + for (int i = 0; i < batchGetValues.getRanges().size(); i++) { + if (i > 0) { + rangesBuilder.append(","); + } + rangesBuilder.append(batchGetValues.getRanges().get(i)); + } + + // 构建基本URL + String baseUrl = BASE_URL + "/sheets/v2/spreadsheets/" + spreadsheetToken + "/values_batch_get"; + + // 构建查询参数 + StringBuilder urlBuilder = new StringBuilder(baseUrl); + urlBuilder.append("?ranges=") + .append(URLEncoder.encode(rangesBuilder.toString(), StandardCharsets.UTF_8.toString())); + + // 添加valueRenderOption参数 + if (batchGetValues.getValueRenderOption() != null && !batchGetValues.getValueRenderOption().isEmpty()) { + urlBuilder.append("&valueRenderOption=").append( + URLEncoder.encode(batchGetValues.getValueRenderOption(), StandardCharsets.UTF_8.toString())); + } + + // 添加dateTimeRenderOption参数 + if (batchGetValues.getDateTimeRenderOption() != null + && !batchGetValues.getDateTimeRenderOption().isEmpty()) { + urlBuilder.append("&dateTimeRenderOption=").append( + URLEncoder.encode(batchGetValues.getDateTimeRenderOption(), StandardCharsets.UTF_8.toString())); + } + + // 添加user_id_type参数 + if (batchGetValues.getUserIdType() != null && !batchGetValues.getUserIdType().isEmpty()) { + urlBuilder.append("&user_id_type=") + .append(URLEncoder.encode(batchGetValues.getUserIdType(), StandardCharsets.UTF_8.toString())); + } + + String url = urlBuilder.toString(); + Request httpRequest = createAuthenticatedRequest(url, "GET", null).build(); + response = executeRequest(httpRequest, ApiResponse.class); + + // 如果请求失败,中断后续请求 + if (!response.success()) { + return response; + } + } + // 处理向单个范围写入数据请求 + else if (valueRequest.getPutValues() != null) { + String url = BASE_URL + "/sheets/v2/spreadsheets/" + spreadsheetToken + "/values"; + + RequestBody body = RequestBody.create(gson.toJson(valueRequest.getPutValues()), JSON_MEDIA_TYPE); + Request httpRequest = createAuthenticatedRequest(url, "PUT", body).build(); + response = executeRequest(httpRequest, ApiResponse.class); + + // 如果请求失败,中断后续请求 + if (!response.success()) { + return response; + } + } + // 处理向多个范围写入数据请求 + else if (valueRequest.getBatchPutValues() != null) { + String url = BASE_URL + "/sheets/v2/spreadsheets/" + spreadsheetToken + "/values_batch_update"; + String params = gson.toJson(valueRequest.getBatchPutValues()); + RequestBody body = RequestBody.create(params, JSON_MEDIA_TYPE); + Request httpRequest = createAuthenticatedRequest(url, "POST", body).build(); + response = executeRequest(httpRequest, ApiResponse.class); + + // 如果请求失败,中断后续请求 + if (!response.success()) { + return response; + } + } + // 这里可以添加其他数据操作类型 + } + + // 如果所有请求都成功处理,返回最后一个成功的响应 + // 如果没有处理任何请求(没有有效的操作类型),返回错误响应 + if (response == null) { + ApiResponse errorResponse = new ApiResponse(); + errorResponse.setCode(400); + errorResponse.setMsg("No valid value operation found"); + return errorResponse; + } + + return response; + } + + /** + * 批量操作数据值请求 + */ + public static class ValueBatchUpdateRequest { + private List requests; + + public ValueBatchUpdateRequest() { + this.requests = new ArrayList<>(); + } + + public List getRequests() { + return requests; + } + + public void setRequests(List requests) { + this.requests = requests; + } + + /** + * 创建批量操作数据值请求的构建器 + * + * @return 批量操作数据值请求的构建器 + */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * 批量操作数据值请求的构建器 + */ + public static class Builder { + private final ValueBatchUpdateRequest request; + + public Builder() { + request = new ValueBatchUpdateRequest(); + } + + /** + * 添加一个数据值操作请求 + * + * @param valueRequest 数据值操作请求 + * @return 当前构建器 + */ + public Builder addRequest(ValueRequest valueRequest) { + request.requests.add(valueRequest); + return this; + } + + /** + * 构建批量操作数据值请求 + * + * @return 批量操作数据值请求 + */ + public ValueBatchUpdateRequest build() { + return request; + } + } + } + + /** + * 数据值操作请求 + */ + public static class ValueRequest { + private ValuePrependRequest prependValues; + private ValueAppendRequest appendValues; + private ValueImageRequest imageValues; + private ValueGetRequest getValues; + private ValueBatchGetRequest batchGetValues; + private ValuePutRequest putValues; + private ValueBatchUpdatePutRequest batchPutValues; + + /** + * 获取在指定范围前插入数据请求 + * + * @return 在指定范围前插入数据请求 + */ + public ValuePrependRequest getPrependValues() { + return prependValues; + } + + /** + * 设置在指定范围前插入数据请求 + * + * @param prependValues 在指定范围前插入数据请求 + */ + public void setPrependValues(ValuePrependRequest prependValues) { + this.prependValues = prependValues; + } + + /** + * 获取在指定范围后追加数据请求 + * + * @return 在指定范围后追加数据请求 + */ + public ValueAppendRequest getAppendValues() { + return appendValues; + } + + /** + * 设置在指定范围后追加数据请求 + * + * @param appendValues 在指定范围后追加数据请求 + */ + public void setAppendValues(ValueAppendRequest appendValues) { + this.appendValues = appendValues; + } + + /** + * 获取写入图片请求 + * + * @return 写入图片请求 + */ + public ValueImageRequest getImageValues() { + return imageValues; + } + + /** + * 设置写入图片请求 + * + * @param imageValues 写入图片请求 + */ + public void setImageValues(ValueImageRequest imageValues) { + this.imageValues = imageValues; + } + + /** + * 获取单个范围数据请求 + * + * @return 获取单个范围数据请求 + */ + public ValueGetRequest getGetValues() { + return getValues; + } + + /** + * 设置获取单个范围数据请求 + * + * @param getValues 获取单个范围数据请求 + */ + public void setGetValues(ValueGetRequest getValues) { + this.getValues = getValues; + } + + /** + * 获取批量获取多个范围数据请求 + * + * @return 批量获取多个范围数据请求 + */ + public ValueBatchGetRequest getBatchGetValues() { + return batchGetValues; + } + + /** + * 设置批量获取多个范围数据请求 + * + * @param batchGetValues 批量获取多个范围数据请求 + */ + public void setBatchGetValues(ValueBatchGetRequest batchGetValues) { + this.batchGetValues = batchGetValues; + } + + /** + * 获取向单个范围写入数据请求 + * + * @return 向单个范围写入数据请求 + */ + public ValuePutRequest getPutValues() { + return putValues; + } + + /** + * 设置向单个范围写入数据请求 + * + * @param putValues 向单个范围写入数据请求 + */ + public void setPutValues(ValuePutRequest putValues) { + this.putValues = putValues; + } + + /** + * 获取向多个范围写入数据请求 + * + * @return 向多个范围写入数据请求 + */ + public ValueBatchUpdatePutRequest getBatchPutValues() { + return batchPutValues; + } + + /** + * 设置向多个范围写入数据请求 + * + * @param batchPutValues 向多个范围写入数据请求 + */ + public void setBatchPutValues(ValueBatchUpdatePutRequest batchPutValues) { + this.batchPutValues = batchPutValues; + } + + /** + * 创建在指定范围前插入数据的请求构建器 + * + * @return 在指定范围前插入数据的构建器 + */ + public static PrependValuesBuilder prependValues() { + return new PrependValuesBuilder(); + } + + /** + * 创建在指定范围后追加数据的请求构建器 + * + * @return 在指定范围后追加数据的构建器 + */ + public static AppendValuesBuilder appendValues() { + return new AppendValuesBuilder(); + } + + /** + * 创建写入图片的请求构建器 + * + * @return 写入图片的构建器 + */ + public static ImageValuesBuilder imageValues() { + return new ImageValuesBuilder(); + } + + /** + * 创建获取单个范围数据的请求构建器 + * + * @return 获取单个范围数据的构建器 + */ + public static GetValuesBuilder getValues() { + return new GetValuesBuilder(); + } + + /** + * 创建批量获取多个范围数据的请求构建器 + * + * @return 批量获取多个范围数据的构建器 + */ + public static BatchGetValuesBuilder batchGetValues() { + return new BatchGetValuesBuilder(); + } + + /** + * 创建向单个范围写入数据的请求构建器 + * + * @return 向单个范围写入数据的构建器 + */ + public static PutValuesBuilder putValues() { + return new PutValuesBuilder(); + } + + /** + * 创建向多个范围写入数据的请求构建器 + * + * @return 向多个范围写入数据的构建器 + */ + public static BatchPutValuesBuilder batchPutValues() { + return new BatchPutValuesBuilder(); + } + + /** + * 在指定范围前插入数据的构建器 + */ + public static class PrependValuesBuilder { + private final ValueRequest request; + private final ValuePrependRequest prependValues; + private final ValueRange valueRange; + + public PrependValuesBuilder() { + request = new ValueRequest(); + prependValues = new ValuePrependRequest(); + valueRange = new ValueRange(); + prependValues.setValueRange(valueRange); + request.setPrependValues(prependValues); + } + + /** + * 设置要插入数据的单元格范围 + * + * @param range 单元格范围,格式为 !<开始位置>:<结束位置> + * @return 当前构建器 + */ + public PrependValuesBuilder range(String range) { + valueRange.setRange(range); + return this; + } + + /** + * 设置要插入数据的单元格范围 + * + * @param sheetId 工作表ID + * @param startPosition 开始位置 + * @param endPosition 结束位置 + * @return 当前构建器 + */ + public PrependValuesBuilder range(String sheetId, String startPosition, String endPosition) { + valueRange.setRange(sheetId + "!" + startPosition + ":" + endPosition); + return this; + } + + /** + * 添加一行数据 + * + * @param rowData 行数据 + * @return 当前构建器 + */ + public PrependValuesBuilder addRow(List rowData) { + valueRange.getValues().add(rowData); + return this; + } + + /** + * 添加一行数据 + * + * @param values 行数据 + * @return 当前构建器 + */ + public PrependValuesBuilder addRow(Object... values) { + List row = new ArrayList<>(); + for (Object value : values) { + row.add(value); + } + valueRange.getValues().add(row); + return this; + } + + /** + * 构建在指定范围前插入数据请求 + * + * @return 数据值操作请求 + */ + public ValueRequest build() { + return request; + } + } + + /** + * 在指定范围后追加数据的构建器 + */ + public static class AppendValuesBuilder { + private final ValueRequest request; + private final ValueAppendRequest appendValues; + private final ValueRange valueRange; + + public AppendValuesBuilder() { + request = new ValueRequest(); + appendValues = new ValueAppendRequest(); + valueRange = new ValueRange(); + appendValues.setValueRange(valueRange); + request.setAppendValues(appendValues); + } + + /** + * 设置要追加数据的单元格范围 + * + * @param range 单元格范围,格式为 !<开始位置>:<结束位置> + * @return 当前构建器 + */ + public AppendValuesBuilder range(String range) { + valueRange.setRange(range); + return this; + } + + /** + * 设置要追加数据的单元格范围 + * + * @param sheetId 工作表ID + * @param startPosition 开始位置 + * @param endPosition 结束位置 + * @return 当前构建器 + */ + public AppendValuesBuilder range(String sheetId, String startPosition, String endPosition) { + valueRange.setRange(sheetId + "!" + startPosition + ":" + endPosition); + return this; + } + + /** + * 设置追加数据的方式 + * + * @param insertDataOption 追加数据方式,可选值:OVERWRITE(默认,覆盖已有数据)、INSERT_ROWS(插入行后追加) + * @return 当前构建器 + */ + public AppendValuesBuilder insertDataOption(String insertDataOption) { + appendValues.setInsertDataOption(insertDataOption); + return this; + } + + /** + * 添加一行数据 + * + * @param rowData 行数据 + * @return 当前构建器 + */ + public AppendValuesBuilder addRow(List rowData) { + valueRange.getValues().add(rowData); + return this; + } + + /** + * 添加一行数据 + * + * @param values 行数据 + * @return 当前构建器 + */ + public AppendValuesBuilder addRow(Object... values) { + List row = new ArrayList<>(); + for (Object value : values) { + row.add(value); + } + valueRange.getValues().add(row); + return this; + } + + /** + * 构建在指定范围后追加数据请求 + * + * @return 数据值操作请求 + */ + public ValueRequest build() { + return request; + } + } + + /** + * 写入图片的构建器 + */ + public static class ImageValuesBuilder { + private final ValueRequest request; + private final ValueImageRequest imageValues; + + public ImageValuesBuilder() { + request = new ValueRequest(); + imageValues = new ValueImageRequest(); + request.setImageValues(imageValues); + } + + /** + * 设置要写入图片的单元格范围 + * + * @param range 单元格范围,格式为 !<单元格位置>:<单元格位置> + * @return 当前构建器 + */ + public ImageValuesBuilder range(String range) { + imageValues.setRange(range); + return this; + } + + /** + * 设置要写入图片的单元格范围 + * + * @param sheetId 工作表ID + * @param position 单元格位置 + * @return 当前构建器 + */ + public ImageValuesBuilder range(String sheetId, String position) { + imageValues.setRange(sheetId + "!" + position + ":" + position); + return this; + } + + /** + * 设置要写入图片的单元格范围 + * + * @param sheetId 工作表ID + * @param startPosition 开始位置 + * @param endPosition 结束位置,应与开始位置相同 + * @return 当前构建器 + */ + public ImageValuesBuilder range(String sheetId, String startPosition, String endPosition) { + imageValues.setRange(sheetId + "!" + startPosition + ":" + endPosition); + return this; + } + + /** + * 设置要写入的图片数据 + * + * @param image 图片二进制数据 + * @return 当前构建器 + */ + public ImageValuesBuilder image(byte[] image) { + imageValues.setImage(image); + return this; + } + + /** + * 设置图片名称 + * + * @param name 图片名称,需要包含后缀名,如"test.png" + * @return 当前构建器 + */ + public ImageValuesBuilder name(String name) { + imageValues.setName(name); + return this; + } + + /** + * 构建写入图片请求 + * + * @return 数据值操作请求 + */ + public ValueRequest build() { + return request; + } + } + + /** + * 获取单个范围数据的构建器 + */ + public static class GetValuesBuilder { + private final ValueRequest request; + private final ValueGetRequest getValues; + + public GetValuesBuilder() { + request = new ValueRequest(); + getValues = new ValueGetRequest(); + request.setGetValues(getValues); + } + + /** + * 设置要获取数据的单元格范围 + * + * @param sheetId 工作表ID + * @param startPosition 开始位置 + * @param endPosition 结束位置 + * @return 当前构建器 + */ + public GetValuesBuilder range(String sheetId, String startPosition, String endPosition) { + getValues.setRange(sheetId + "!" + startPosition + ":" + endPosition); + return this; + } + + /** + * 设置单元格数据的格式 + * + * @param valueRenderOption 单元格数据格式,可选值: - ToString:返回纯文本的值(数值类型除外) - Formula:单元格中含有公式时,返回公式本身 - + * FormattedValue:计算并格式化单元格 - UnformattedValue:计算但不对单元格进行格式化 + * @return 当前构建器 + */ + public GetValuesBuilder valueRenderOption(String valueRenderOption) { + getValues.setValueRenderOption(valueRenderOption); + return this; + } + + /** + * 设置日期时间单元格数据的格式 + * + * @param dateTimeRenderOption 日期时间单元格数据格式,可选值: - FormattedString:返回格式化后的字符串 + * @return 当前构建器 + */ + public GetValuesBuilder dateTimeRenderOption(String dateTimeRenderOption) { + getValues.setDateTimeRenderOption(dateTimeRenderOption); + return this; + } + + /** + * 设置用户ID类型 + * + * @param userIdType 用户ID类型,可选值: - open_id:标识一个用户在某个应用中的身份 - union_id:标识一个用户在某个应用开发商下的身份 + * @return 当前构建器 + */ + public GetValuesBuilder userIdType(String userIdType) { + getValues.setUserIdType(userIdType); + return this; + } + + /** + * 构建获取单个范围数据请求 + * + * @return 数据值操作请求 + */ + public ValueRequest build() { + return request; + } + } + + /** + * 批量获取多个范围数据的构建器 + */ + public static class BatchGetValuesBuilder { + private final ValueRequest request; + private final ValueBatchGetRequest batchGetValues; + + public BatchGetValuesBuilder() { + request = new ValueRequest(); + batchGetValues = new ValueBatchGetRequest(); + request.setBatchGetValues(batchGetValues); + } + + /** + * 添加要获取数据的单元格范围 + * + * @param range 单元格范围,格式为 !<开始位置>:<结束位置> + * @return 当前构建器 + */ + public BatchGetValuesBuilder addRange(String range) { + batchGetValues.addRange(range); + return this; + } + + /** + * 添加要获取数据的单元格范围 + * + * @param sheetId 工作表ID + * @param startPosition 开始位置 + * @param endPosition 结束位置 + * @return 当前构建器 + */ + public BatchGetValuesBuilder addRange(String sheetId, String startPosition, String endPosition) { + String range = sheetId + "!" + startPosition + ":" + endPosition; + batchGetValues.addRange(range); + return this; + } + + /** + * 设置单元格数据的格式 + * + * @param valueRenderOption 单元格数据格式,可选值: - ToString:返回纯文本的值(数值类型除外) - Formula:单元格中含有公式时,返回公式本身 - + * FormattedValue:计算并格式化单元格 - UnformattedValue:计算但不对单元格进行格式化 + * @return 当前构建器 + */ + public BatchGetValuesBuilder valueRenderOption(String valueRenderOption) { + batchGetValues.setValueRenderOption(valueRenderOption); + return this; + } + + /** + * 设置日期时间单元格数据的格式 + * + * @param dateTimeRenderOption 日期时间单元格数据格式,可选值: - FormattedString:返回格式化后的字符串 + * @return 当前构建器 + */ + public BatchGetValuesBuilder dateTimeRenderOption(String dateTimeRenderOption) { + batchGetValues.setDateTimeRenderOption(dateTimeRenderOption); + return this; + } + + /** + * 设置用户ID类型 + * + * @param userIdType 用户ID类型,可选值: - open_id:标识一个用户在某个应用中的身份 - union_id:标识一个用户在某个应用开发商下的身份 + * @return 当前构建器 + */ + public BatchGetValuesBuilder userIdType(String userIdType) { + batchGetValues.setUserIdType(userIdType); + return this; + } + + /** + * 构建批量获取多个范围数据请求 + * + * @return 数据值操作请求 + */ + public ValueRequest build() { + return request; + } + } + + /** + * 向单个范围写入数据的构建器 + */ + public static class PutValuesBuilder { + private final ValueRequest request; + private final ValuePutRequest putValues; + private final ValueRange valueRange; + + public PutValuesBuilder() { + request = new ValueRequest(); + putValues = new ValuePutRequest(); + valueRange = new ValueRange(); + putValues.setValueRange(valueRange); + request.setPutValues(putValues); + } + + /** + * 设置要写入数据的单元格范围 + * + * @param sheetId 工作表ID + * @param startPosition 开始位置 + * @param endPosition 结束位置 + * @return 当前构建器 + */ + public PutValuesBuilder range(String sheetId, String startPosition, String endPosition) { + valueRange.setRange(sheetId + "!" + startPosition + ":" + endPosition); + return this; + } + + /** + * 添加一行数据 + * + * @param rowData 行数据 + * @return 当前构建器 + */ + public PutValuesBuilder addRow(List rowData) { + valueRange.getValues().add(rowData); + return this; + } + + /** + * 添加一行数据 + * + * @param values 行数据 + * @return 当前构建器 + */ + public PutValuesBuilder addRow(Object... values) { + List row = new ArrayList<>(); + for (Object value : values) { + row.add(value); + } + valueRange.getValues().add(row); + return this; + } + + /** + * 构建向单个范围写入数据请求 + * + * @return 数据值操作请求 + */ + public ValueRequest build() { + return request; + } + } + + /** + * 向多个范围写入数据的构建器 + */ + public static class BatchPutValuesBuilder { + private final ValueRequest request; + private final ValueBatchUpdatePutRequest batchPutValues; + private ValueRangeItem currentItem; + + public BatchPutValuesBuilder() { + request = new ValueRequest(); + batchPutValues = new ValueBatchUpdatePutRequest(); + request.setBatchPutValues(batchPutValues); + } + + public BatchPutValuesBuilder setReqType(String reqType) { + batchPutValues.setType(reqType); + return this; + } + + public BatchPutValuesBuilder setReqParams(String reqParams) { + batchPutValues.setParams(reqParams); + return this; + } + + /** + * 添加一个新的范围 + * + * @param range 单元格范围,格式为 !<开始位置>:<结束位置> + * @return 当前构建器 + */ + public BatchPutValuesBuilder addRange(String range) { + currentItem = new ValueRangeItem(); + currentItem.setRange(range); + batchPutValues.getValueRanges().add(currentItem); + return this; + } + + public BatchPutValuesBuilder setType(String type) { + currentItem.setType(type); + return this; + } + + /** + * 添加一个新的范围 + * + * @param sheetId 工作表ID + * @param startPosition 开始位置 + * @param endPosition 结束位置 + * @return 当前构建器 + */ + public BatchPutValuesBuilder addRange(String sheetId, String startPosition, String endPosition) { + String range = sheetId + "!" + startPosition + ":" + endPosition; + return addRange(range); + } + + /** + * 为当前范围添加一行数据 + * + * @param rowData 行数据 + * @return 当前构建器 + */ + public BatchPutValuesBuilder addRow(List rowData) { + if (currentItem == null) { + throw new IllegalStateException("Must call addRange before addRow"); + } + currentItem.getValues().add(rowData); + return this; + } + + /** + * 为当前范围添加一行数据 + * + * @param values 行数据 + * @return 当前构建器 + */ + public BatchPutValuesBuilder addRow(Object... values) { + if (currentItem == null) { + throw new IllegalStateException("Must call addRange before addRow"); + } + List row = new ArrayList<>(); + for (Object value : values) { + row.add(value); + } + currentItem.getValues().add(row); + return this; + } + + /** + * 构建向多个范围写入数据请求 + * + * @return 数据值操作请求 + */ + public ValueRequest build() { + return request; + } + } + } + + /** + * 值范围 + */ + public static class ValueRange { + private String range; + private List> values; + + public ValueRange() { + this.values = new ArrayList<>(); + } + + public String getRange() { + return range; + } + + public void setRange(String range) { + this.range = range; + } + + public List> getValues() { + return values; + } + + public void setValues(List> values) { + this.values = values; + } + } + + /** + * 在指定范围前插入数据请求 + */ + public static class ValuePrependRequest { + private ValueRange valueRange; + + public ValuePrependRequest() {} + + public ValueRange getValueRange() { + return valueRange; + } + + public void setValueRange(ValueRange valueRange) { + this.valueRange = valueRange; + } + + /** + * 创建插入数据请求的构建器 + * + * @return 插入数据请求的构建器 + */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * 插入数据请求的构建器 + */ + public static class Builder { + private final ValuePrependRequest request; + private final ValueRange valueRange; + + public Builder() { + request = new ValuePrependRequest(); + valueRange = new ValueRange(); + request.setValueRange(valueRange); + } + + /** + * 设置要插入数据的单元格范围 + * + * @param sheetId 工作表ID + * @param startPosition 开始位置 + * @param endPosition 结束位置 + * @return 当前构建器 + */ + public Builder range(String sheetId, String startPosition, String endPosition) { + valueRange.setRange(sheetId + "!" + startPosition + ":" + endPosition); + return this; + } + + /** + * 添加一行数据 + * + * @param rowData 行数据 + * @return 当前构建器 + */ + public Builder addRow(List rowData) { + valueRange.getValues().add(rowData); + return this; + } + + /** + * 添加一行数据 + * + * @param values 行数据 + * @return 当前构建器 + */ + public Builder addRow(Object... values) { + List row = new ArrayList<>(); + for (Object value : values) { + row.add(value); + } + valueRange.getValues().add(row); + return this; + } + + /** + * 构建插入数据请求 + * + * @return 插入数据请求 + */ + public ValuePrependRequest build() { + return request; + } + } + } + + /** + * 在指定范围后追加数据请求 + */ + public static class ValueAppendRequest { + private ValueRange valueRange; + private String insertDataOption; + + public ValueAppendRequest() {} + + public ValueRange getValueRange() { + return valueRange; + } + + public void setValueRange(ValueRange valueRange) { + this.valueRange = valueRange; + } + + /** + * 获取追加数据的方式 + * + * @return 追加数据方式,可选值:OVERWRITE(默认,覆盖已有数据)、INSERT_ROWS(插入行后追加) + */ + public String getInsertDataOption() { + return insertDataOption; + } + + /** + * 设置追加数据的方式 + * + * @param insertDataOption 追加数据方式,可选值:OVERWRITE(默认,覆盖已有数据)、INSERT_ROWS(插入行后追加) + */ + public void setInsertDataOption(String insertDataOption) { + this.insertDataOption = insertDataOption; + } + + /** + * 创建追加数据请求的构建器 + * + * @return 追加数据请求的构建器 + */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * 追加数据请求的构建器 + */ + public static class Builder { + private final ValueAppendRequest request; + private final ValueRange valueRange; + + public Builder() { + request = new ValueAppendRequest(); + valueRange = new ValueRange(); + request.setValueRange(valueRange); + } + + /** + * 设置要追加数据的单元格范围 + * + * @param sheetId 工作表ID + * @param startPosition 开始位置 + * @param endPosition 结束位置 + * @return 当前构建器 + */ + public Builder range(String sheetId, String startPosition, String endPosition) { + valueRange.setRange(sheetId + "!" + startPosition + ":" + endPosition); + return this; + } + + /** + * 添加一行数据 + * + * @param rowData 行数据 + * @return 当前构建器 + */ + public Builder addRow(List rowData) { + valueRange.getValues().add(rowData); + return this; + } + + /** + * 添加一行数据 + * + * @param values 行数据 + * @return 当前构建器 + */ + public Builder addRow(Object... values) { + List row = new ArrayList<>(); + for (Object value : values) { + row.add(value); + } + valueRange.getValues().add(row); + return this; + } + + /** + * 构建追加数据请求 + * + * @return 追加数据请求 + */ + public ValueAppendRequest build() { + return request; + } + } + } + + /** + * 写入图片请求 + */ + public static class ValueImageRequest { + private String range; + private byte[] image; + private String name; + + public ValueImageRequest() {} + + /** + * 获取单元格范围 + * + * @return 单元格范围 + */ + public String getRange() { + return range; + } + + /** + * 设置单元格范围 + * + * @param range 单元格范围,格式为 !<单元格位置>:<单元格位置> + */ + public void setRange(String range) { + this.range = range; + } + + /** + * 获取图片数据 + * + * @return 图片二进制数据 + */ + public byte[] getImage() { + return image; + } + + /** + * 设置图片数据 + * + * @param image 图片二进制数据 + */ + public void setImage(byte[] image) { + this.image = image; + } + + /** + * 获取图片名称 + * + * @return 图片名称 + */ + public String getName() { + return name; + } + + /** + * 设置图片名称 + * + * @param name 图片名称,需要包含后缀名,如"test.png" + */ + public void setName(String name) { + this.name = name; + } + } + + /** + * 获取单个范围数据请求 + */ + public static class ValueGetRequest { + private String range; + private String valueRenderOption; + private String dateTimeRenderOption; + private String userIdType; + + public ValueGetRequest() {} + + /** + * 获取单元格范围 + * + * @return 单元格范围 + */ + public String getRange() { + return range; + } + + /** + * 设置单元格范围 + * + * @param range 单元格范围,格式为 !<开始位置>:<结束位置> + */ + public void setRange(String range) { + this.range = range; + } + + /** + * 获取单元格数据格式 + * + * @return 单元格数据格式 + */ + public String getValueRenderOption() { + return valueRenderOption; + } + + /** + * 设置单元格数据格式 + * + * @param valueRenderOption 单元格数据格式 + */ + public void setValueRenderOption(String valueRenderOption) { + this.valueRenderOption = valueRenderOption; + } + + /** + * 获取日期时间单元格数据格式 + * + * @return 日期时间单元格数据格式 + */ + public String getDateTimeRenderOption() { + return dateTimeRenderOption; + } + + /** + * 设置日期时间单元格数据格式 + * + * @param dateTimeRenderOption 日期时间单元格数据格式 + */ + public void setDateTimeRenderOption(String dateTimeRenderOption) { + this.dateTimeRenderOption = dateTimeRenderOption; + } + + /** + * 获取用户ID类型 + * + * @return 用户ID类型 + */ + public String getUserIdType() { + return userIdType; + } + + /** + * 设置用户ID类型 + * + * @param userIdType 用户ID类型 + */ + public void setUserIdType(String userIdType) { + this.userIdType = userIdType; + } + } + + /** + * 批量获取多个范围数据请求 + */ + public static class ValueBatchGetRequest { + private List ranges; + private String valueRenderOption; + private String dateTimeRenderOption; + private String userIdType; + + private String type; + private String params; + + public ValueBatchGetRequest() { + this.ranges = new ArrayList<>(); + } + + /** + * 获取单元格范围列表 + * + * @return 单元格范围列表 + */ + public List getRanges() { + return ranges; + } + + /** + * 设置单元格范围列表 + * + * @param ranges 单元格范围列表 + */ + public void setRanges(List ranges) { + this.ranges = ranges; + } + + /** + * 添加单元格范围 + * + * @param range 单元格范围,格式为 !<开始位置>:<结束位置> + */ + public void addRange(String range) { + if (this.ranges == null) { + this.ranges = new ArrayList<>(); + } + this.ranges.add(range); + } + + /** + * 获取单元格数据格式 + * + * @return 单元格数据格式 + */ + public String getValueRenderOption() { + return valueRenderOption; + } + + /** + * 设置单元格数据格式 + * + * @param valueRenderOption 单元格数据格式 + */ + public void setValueRenderOption(String valueRenderOption) { + this.valueRenderOption = valueRenderOption; + } + + /** + * 获取日期时间单元格数据格式 + * + * @return 日期时间单元格数据格式 + */ + public String getDateTimeRenderOption() { + return dateTimeRenderOption; + } + + /** + * 设置日期时间单元格数据格式 + * + * @param dateTimeRenderOption 日期时间单元格数据格式 + */ + public void setDateTimeRenderOption(String dateTimeRenderOption) { + this.dateTimeRenderOption = dateTimeRenderOption; + } + + /** + * 获取用户ID类型 + * + * @return 用户ID类型 + */ + public String getUserIdType() { + return userIdType; + } + + /** + * 设置用户ID类型 + * + * @param userIdType 用户ID类型 + */ + public void setUserIdType(String userIdType) { + this.userIdType = userIdType; + } + + public void setType(String type) { + this.type = type; + } + + public void setParams(String params) { + this.params = params; + } + + public String getType() { + return type; + } + + public String getParams() { + return params; + } + } + + /** + * 向单个范围写入数据请求 + */ + public static class ValuePutRequest { + private ValueRange valueRange; + + public ValuePutRequest() {} + + /** + * 获取值范围 + * + * @return 值范围 + */ + public ValueRange getValueRange() { + return valueRange; + } + + /** + * 设置值范围 + * + * @param valueRange 值范围 + */ + public void setValueRange(ValueRange valueRange) { + this.valueRange = valueRange; + } + } + + /** + * 向多个范围写入数据请求 + */ + public static class ValueBatchUpdatePutRequest { + private List valueRanges; + private String type; + private String params; + + public ValueBatchUpdatePutRequest() { + this.valueRanges = new ArrayList<>(); + } + + /** + * 获取值范围列表 + * + * @return 值范围列表 + */ + public List getValueRanges() { + return valueRanges; + } + + /** + * 设置值范围列表 + * + * @param valueRanges 值范围列表 + */ + public void setValueRanges(List valueRanges) { + this.valueRanges = valueRanges; + } + + public void setType(String type) { + this.type = type; + } + + public void setParams(String params) { + this.params = params; + } + + public String getType() { + return type; + } + + public String getParams() { + return params; + } + } + + /** + * 值范围项 + */ + public static class ValueRangeItem { + private String range; + private String type; + private List> values; + + public ValueRangeItem() { + this.values = new ArrayList<>(); + } + + /** + * 获取单元格范围 + * + * @return 单元格范围 + */ + public String getRange() { + return range; + } + + /** + * 设置单元格范围 + * + * @param range 单元格范围,格式为 !<开始位置>:<结束位置> + */ + public void setRange(String range) { + this.range = range; + } + + /** + * 获取数据值 + * + * @return 数据值 + */ + public List> getValues() { + return values; + } + + /** + * 设置数据值 + * + * @param values 数据值 + */ + public void setValues(List> values) { + this.values = values; + } + + + public void setType(String type) { + this.type = type; + } + + public String getType() { + return type; + } + } +} \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/utils/ConvertFieldUtil.java b/src/main/java/cn/isliu/core/utils/ConvertFieldUtil.java new file mode 100644 index 0000000..c8152b5 --- /dev/null +++ b/src/main/java/cn/isliu/core/utils/ConvertFieldUtil.java @@ -0,0 +1,235 @@ +package cn.isliu.core.utils; + +import cn.isliu.core.annotation.TableProperty; +import cn.isliu.core.converters.FieldValueProcess; +import cn.isliu.core.enums.BaseEnum; +import cn.isliu.core.enums.TypeEnum; +import cn.isliu.core.pojo.FieldProperty; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import java.lang.reflect.InvocationTargetException; + +public class ConvertFieldUtil { + private static final Logger log = Logger.getLogger(ConvertFieldUtil.class.getName()); + private static final Gson gson = new Gson(); + + public static Map convertPositionToField(JsonObject jsonObject, Map fieldsMap) { + Map result = new HashMap<>(); + + for (Map.Entry entry : jsonObject.entrySet()) { + String positionKey = entry.getKey(); + FieldProperty fieldProperty = fieldsMap.get(positionKey); + if (fieldProperty == null) continue; + String fieldKey = fieldProperty.getField(); + TableProperty tableProperty = fieldProperty.getTableProperty(); + + + Object value = getValueByFieldRule(tableProperty, entry.getValue()); + if (fieldKey != null) { + // 根据配置获取值 + result.put(fieldKey, value); + } else { + // 未找到对应配置项时保持原键(可选) + result.put(positionKey, value); + } + } + + return result; + } + + private static Object getValueByFieldRule(TableProperty tableProperty, JsonElement value) { + if (tableProperty == null || value == null || value.isJsonNull()) { + return null; + } + Object result = null; + + TypeEnum typeEnum = tableProperty.type(); + + if (Objects.nonNull(typeEnum)) { + switch (typeEnum) { + case TEXT: + case NUMBER: + case DATE: + // 直接获取值,避免额外的引号 + result = getJsonValue(value); + break; + + case SINGLE_SELECT: + List arr = parseStrToArr(value); + result = conversionValue(tableProperty, arr.get(0)); + break; + + case MULTI_TEXT: + result = parseStrToArr(value); + break; + + case MULTI_SELECT: + List values = parseStrToArr(value); + result = values.stream() + .map(v -> conversionValue(tableProperty, v)).collect(Collectors.toList()); + break; + + case TEXT_URL: + result = getTextUrl(value); + break; + + case TEXT_FILE: + result = conversionValue(tableProperty, getJsonValue(value)); + break; + } + } + + return result; + } + + private static Object getTextUrl(JsonElement value) { + if (value instanceof JsonArray) { + List fileUrls = new ArrayList<>(); + JsonArray arr = (JsonArray) value; + for (int i = 0; i < arr.size(); i++) { + JsonElement jsonElement = arr.get(i); + if (jsonElement.isJsonObject()) { + JsonObject jsonObject = jsonElement.getAsJsonObject(); + String url = getUrlByTextFile(jsonObject); + if (!url.isEmpty()) { + fileUrls.add(url); + } + } + } + return String.join(",", fileUrls); + } else if (value instanceof JsonObject) { + JsonObject jsb = (JsonObject) value; + return getUrlByTextFile(jsb); + } + return value; + } + + /** + * 从JsonElement中提取合适的值,避免额外的引号 + */ + private static Object getJsonValue(JsonElement value) { + if (value.isJsonPrimitive()) { + // 检查是否为字符串 + if (value.getAsJsonPrimitive().isString()) { + String strValue = value.getAsString(); + // 检查字符串是否以引号开始和结束,如果是则去除引号 + if (strValue.length() >= 2 && + ((strValue.startsWith("\"") && strValue.endsWith("\"")) || + (strValue.startsWith("'") && strValue.endsWith("'")))) { + return strValue.substring(1, strValue.length() - 1); + } + return strValue; + } else if (value.getAsJsonPrimitive().isNumber()) { + return value.getAsNumber(); + } else if (value.getAsJsonPrimitive().isBoolean()) { + return value.getAsBoolean(); + } + } + return value; + } + + private static String getUrlByTextFile(JsonObject jsb) { + String url = ""; + String cellType = jsb.get("type").getAsString(); + + if (cellType.equals("url")) { + String link = jsb.get("link").getAsString(); + if (link == null) { + url = jsb.get("text").getAsString(); + } else { + url = link; + } + } + return url; + } + + public static List parseStrToArr(JsonElement value) { + String result = ""; + if (value.isJsonPrimitive()) { + if (value.getAsJsonPrimitive().isString()) { + result = value.getAsString(); + // 检查字符串是否以引号开始和结束,如果是则去除引号 + if (result.length() >= 2 && + ((result.startsWith("\"") && result.endsWith("\"")) || + (result.startsWith("'") && result.endsWith("'")))) { + result = result.substring(1, result.length() - 1); + } + } else { + result = value.toString(); + } + } else if (value.isJsonArray()) { + // 处理数组类型 + result = value.toString(); + } else { + result = value.toString(); + } + + String[] split = result.split(","); + return new ArrayList<>(Arrays.asList(split)); + } + + private static Object conversionValue(TableProperty tableProperty, Object value) { + Object result = value; + if (value != null) { + Class enumClass = tableProperty.enumClass(); + if (enumClass != null && enumClass != BaseEnum.class) { + BaseEnum baseEnum = BaseEnum.getByDesc(enumClass, value); + if (baseEnum != null) { + result = baseEnum.getCode(); + } + } + + Class fieldFormatClass = tableProperty.fieldFormatClass(); + if (fieldFormatClass != null && !fieldFormatClass.isInterface()) { + try { + // 使用更安全的实例化方式 + FieldValueProcess fieldValueProcess = fieldFormatClass.getDeclaredConstructor().newInstance(); + result = fieldValueProcess.process(result); + } catch (InstantiationException e) { + log.log(Level.SEVERE, "无法实例化字段格式化类: " + fieldFormatClass.getName(), e); + } catch (IllegalAccessException e) { + log.log(Level.SEVERE, "无法访问字段格式化类的构造函数: " + fieldFormatClass.getName(), e); + } catch (NoSuchMethodException e) { + log.log(Level.SEVERE, "字段格式化类缺少无参构造函数: " + fieldFormatClass.getName(), e); + } catch (InvocationTargetException e) { + log.log(Level.SEVERE, "字段格式化类构造函数调用异常: " + fieldFormatClass.getName(), e); + } catch (Exception e) { + log.log(Level.SEVERE, "创建字段格式化类实例时发生未知异常: " + fieldFormatClass.getName(), e); + } + } + } + return result; + } + + public static Object reverseValueConversion(TableProperty tableProperty, Object value) { + Object result = value; + if (value != null && tableProperty != null) { + Class enumClass = tableProperty.enumClass(); + if (enumClass != null && enumClass != BaseEnum.class) { + BaseEnum baseEnum = BaseEnum.getByCode(enumClass, value); + if (baseEnum != null) { + result = baseEnum.getDesc(); + } + } + + Class fieldFormatClass = tableProperty.fieldFormatClass(); + if (fieldFormatClass != null && !fieldFormatClass.isInterface()) { + try { + FieldValueProcess fieldValueProcess = fieldFormatClass.newInstance(); + result = fieldValueProcess.reverseProcess(result); + } catch (InstantiationException | IllegalAccessException e) { + log.log(Level.FINE, "format value error", e); + } + } + } + return result; + } +} \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/utils/FileUtil.java b/src/main/java/cn/isliu/core/utils/FileUtil.java new file mode 100644 index 0000000..5dc6f0c --- /dev/null +++ b/src/main/java/cn/isliu/core/utils/FileUtil.java @@ -0,0 +1,121 @@ +package cn.isliu.core.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.file.Files; + +public class FileUtil { + + private static final Logger log = LoggerFactory.getLogger(FileUtil.class); + public static final String DEFAULT_FILE_PATH = "fs"; + + public static byte [] getImageData(String filePath) { + byte[] imageData = new byte[0]; + try { + // 如果 filePath 是 URL,则使用网络请求获取图片 + if (filePath.startsWith("http")) { + URL url = new URL(filePath); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setDoInput(true); + connection.connect(); + + try (InputStream input = connection.getInputStream()) { + imageData = readStream(input); + } + } else { + // 否则当作本地文件处理 + File imageFile = new File(filePath); + imageData = Files.readAllBytes(imageFile.toPath()); + } + } catch (IOException e) { + log.error("【巨量广告助手】 读取图片文件异常!参数:{},错误信息:{}", filePath, e.getMessage(), e); + } + return imageData; + } + + public static byte[] readStream(InputStream input) throws IOException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = input.read(buffer)) != -1) { + byteArrayOutputStream.write(buffer, 0, bytesRead); + } + return byteArrayOutputStream.toByteArray(); + } + + /** + * 根据 URL 下载文件到本地路径 + * + * @param fileURL 文件的 URL 地址 + * @param savePath 保存文件的本地路径 + * @throws IOException 如果下载或写入过程中发生错误 + */ + public static void downloadFile(String fileURL, String savePath) { + HttpURLConnection httpConn = null; + try { + URL url = new URL(fileURL); + + // 打开连接 + httpConn = (HttpURLConnection) url.openConnection(); + int responseCode = httpConn.getResponseCode(); + + // 检查响应码是否为 HTTP OK + if (responseCode == HttpURLConnection.HTTP_OK) { + // 获取输入流 + InputStream inputStream = httpConn.getInputStream(); + + // 创建文件输出流 + FileOutputStream outputStream = new FileOutputStream(savePath); + + // 缓冲区 + byte[] buffer = new byte[1024]; + int bytesRead; + + // 写入文件 + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + + // 关闭流 + outputStream.close(); + inputStream.close(); + + log.info("文件下载成功。保存路径: {}", savePath); + } else { + log.error("文件下载失败。响应码: {}", responseCode); + } + } catch (Exception e) { + log.error("文件下载失败。", e); + } finally { + if (httpConn != null) { + httpConn.disconnect(); + } + } + } + + /** + * 获取系统临时目录路径,并确保目录存在 + * @return 临时目录路径 + */ + public static String getRootPath() { + // 获取系统临时目录 + String tempDir = System.getProperty("java.io.tmpdir") + File.separator + DEFAULT_FILE_PATH ; + // 确保路径以文件分隔符结尾 + if (!tempDir.endsWith(File.separator)) { + tempDir += File.separator; + } + + // 确保目录存在 + File dir = new File(tempDir); + if (!dir.exists()) { + dir.mkdirs(); + } + + return tempDir; + } +} diff --git a/src/main/java/cn/isliu/core/utils/FsApiUtil.java b/src/main/java/cn/isliu/core/utils/FsApiUtil.java new file mode 100644 index 0000000..8fad513 --- /dev/null +++ b/src/main/java/cn/isliu/core/utils/FsApiUtil.java @@ -0,0 +1,488 @@ +package cn.isliu.core.utils; + +import cn.isliu.core.CopySheet; +import cn.isliu.core.Reply; +import cn.isliu.core.Sheet; +import cn.isliu.core.SheetMeta; +import cn.isliu.core.ValuesBatch; +import cn.isliu.core.client.FeishuClient; +import cn.isliu.core.exception.FsHelperException; +import cn.isliu.core.pojo.ApiResponse; +import cn.isliu.core.service.*; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.lark.oapi.service.drive.v1.model.BatchGetTmpDownloadUrlMediaReq; +import com.lark.oapi.service.drive.v1.model.BatchGetTmpDownloadUrlMediaResp; +import com.lark.oapi.service.drive.v1.model.DownloadMediaReq; +import com.lark.oapi.service.drive.v1.model.DownloadMediaResp; +import com.lark.oapi.service.sheets.v3.model.GetSpreadsheetReq; +import com.lark.oapi.service.sheets.v3.model.GetSpreadsheetResp; +import com.lark.oapi.service.sheets.v3.model.QuerySpreadsheetSheetReq; +import com.lark.oapi.service.sheets.v3.model.QuerySpreadsheetSheetResp; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Logger; +import java.util.logging.Level; + + +public class FsApiUtil { + + private static final Gson gson = new Gson(); + private static final Logger log = Logger.getLogger(FsApiUtil.class.getName()); + private static final String REQ_TYPE = "JSON_STR"; + public static final int DEFAULT_ROW_NUM = 1000; + + public static ValuesBatch getSheetData(String sheetId, String spreadsheetToken, String startPosition, String endPosition, FeishuClient client) { + ValuesBatch valuesBatch = null; + try { + CustomValueService.ValueRequest batchGetRequest = CustomValueService.ValueRequest.batchGetValues() + .addRange(sheetId, startPosition, endPosition) + .valueRenderOption("Formula") + .dateTimeRenderOption("FormattedString") + .build(); + + CustomValueService.ValueBatchUpdateRequest getBatchRangesRequest = CustomValueService.ValueBatchUpdateRequest.newBuilder() + .addRequest(batchGetRequest) + .build(); + + ApiResponse batchRangeResp = client.customValues().valueBatchUpdate(spreadsheetToken, getBatchRangesRequest); + + if (batchRangeResp.success()) { + valuesBatch = gson.fromJson(gson.toJson(batchRangeResp.getData()), ValuesBatch.class); + } else { + log.log(Level.SEVERE, "【飞书表格】获取Sheet数据失败! 错误信息:{0}", gson.toJson(batchRangeResp)); + throw new FsHelperException("【飞书表格】获取Sheet数据失败!"); + } + } catch (Exception e) { + log.log(Level.SEVERE, "【飞书表格】获取Sheet数据失败! 错误信息:{0}", e.getMessage()); + throw new FsHelperException("【飞书表格】获取Sheet数据失败!"); + } + return valuesBatch; + } + + public static Sheet getSheetMetadata(String sheetId, FeishuClient client, String spreadsheetToken) { + try { + QuerySpreadsheetSheetReq req = QuerySpreadsheetSheetReq.newBuilder() + .spreadsheetToken(spreadsheetToken) + .build(); + + // 发起请求 + QuerySpreadsheetSheetResp resp = client.sheets().v3().spreadsheetSheet().query(req); + + // 处理服务端错误 + if (resp.success()) { + // 修复参数转换遗漏问题 - 直接使用添加了注解的类进行转换 + SheetMeta sheetMeta = gson.fromJson(gson.toJson(resp.getData()), SheetMeta.class); + List sheets = sheetMeta.getSheets(); + + AtomicReference sheet = new AtomicReference<>(); + sheets.forEach(s -> { + if (s.getSheetId().equals(sheetId)) { + sheet.set(s); + } + }); + + return sheet.get(); + } else { + log.log(Level.SEVERE, "【飞书表格】 获取Sheet元数据异常!错误信息:{0}", gson.toJson(resp)); + throw new FsHelperException("【飞书表格】 获取Sheet元数据异常!错误信息:" + resp.getMsg()); + } + + } catch (Exception e) { + log.log(Level.SEVERE, "【飞书表格】 获取Sheet元数据异常!错误信息:{0}", e.getMessage()); + throw new FsHelperException("【飞书表格】 获取Sheet元数据异常!"); + } + } + + public static void mergeCells(String cell, String sheetId, FeishuClient client, String spreadsheetToken) { + try { + CustomCellService.CellBatchUpdateRequest batchMergeRequest = CustomCellService.CellBatchUpdateRequest.newBuilder() + .addRequest(CustomCellService.CellRequest.mergeCells().setReqType(REQ_TYPE) + .setReqParams(cell.replaceAll("%SHEET_ID%", sheetId)).build()) + .build(); + + ApiResponse batchMergeResp = client.customCells().cellsBatchUpdate(spreadsheetToken, batchMergeRequest); + + if (!batchMergeResp.success()) { + log.log(Level.SEVERE, "【飞书表格】 合并单元格请求异常!参数:{0},错误信息:{1}", new Object[]{cell, batchMergeResp.getMsg()}); + throw new FsHelperException("【飞书表格】 合并单元格请求异常!"); + } + } catch (Exception e) { + log.log(Level.SEVERE, "【飞书表格】 合并单元格异常!参数:{0},错误信息:{1}", new Object[]{cell, e.getMessage()}); + throw new FsHelperException("【飞书表格】 合并单元格异常!"); + } + } + + public static void createTemplateHead(String head, String sheetId, FeishuClient client, String spreadsheetToken) { + try { + // 批量操作数据值(在一个请求中同时执行多个数据操作) + CustomValueService.ValueBatchUpdateRequest batchValueRequest = CustomValueService.ValueBatchUpdateRequest.newBuilder() + // 在指定范围前插入数据 + .addRequest(CustomValueService.ValueRequest.batchPutValues() + .setReqType(REQ_TYPE) + .setReqParams(head.replaceAll("%SHEET_ID%", sheetId)) + .build()) + .build(); + + ApiResponse apiResponse = client.customValues().valueBatchUpdate(spreadsheetToken, batchValueRequest); + if (!apiResponse.success()) { + log.log(Level.SEVERE, "【飞书表格】 写入表格头数据异常!错误信息:{0}", apiResponse.getMsg()); + throw new FsHelperException("【飞书表格】 写入表格头数据异常!"); + } + } catch (Exception e) { + log.log(Level.SEVERE, "【飞书表格】 写入表格头异常!错误信息:{0}", e.getMessage()); + throw new FsHelperException("【飞书表格】 写入表格头异常!"); + } + } + + public static void setTableStyle(String style, String sheetId, FeishuClient client, String spreadsheetToken) { + try { + CustomCellService.CellBatchUpdateRequest batchUpdateRequest = CustomCellService.CellBatchUpdateRequest.newBuilder() + .addRequest(CustomCellService.CellRequest.styleCellsBatch().setReqType(REQ_TYPE) + .setParams(style.replaceAll("%SHEET_ID%", sheetId)) + .build()) + .build(); + + ApiResponse apiResponse = client.customCells().cellsBatchUpdate(spreadsheetToken, batchUpdateRequest); + if (!apiResponse.success()) { + log.log(Level.SEVERE, "【飞书表格】 写入表格样式数据异常!参数:{0},错误信息:{1}", new Object[]{style, apiResponse.getMsg()}); + throw new FsHelperException("【飞书表格】 写入表格样式数据异常!"); + } + } catch (Exception e) { + log.log(Level.SEVERE, "【飞书表格】 写入表格样式异常!参数:{0},错误信息:{1}", new Object[]{style, e.getMessage()}); + throw new FsHelperException("【飞书表格】 写入表格样式异常!"); + } + } + + public static String createSheet(String title, FeishuClient client, String spreadsheetToken) { + String sheetId = null; + try { + // 创建 sheet + CustomSheetService.SheetBatchUpdateRequest addSheetRequest = CustomSheetService.SheetBatchUpdateRequest.newBuilder() + .addRequest(CustomSheetService.SheetRequest.addSheet() + .title(title) +// .index(0) // 在第一个位置添加 + .build()) + .build(); + + ApiResponse addResp = client.customSheets().sheetsBatchUpdate(spreadsheetToken, addSheetRequest); + + if (addResp.success()) { + log.log(Level.INFO, "【飞书表格】 创建 sheet 成功! {0}", gson.toJson(addResp)); + + JsonObject jsObj = gson.fromJson(gson.toJson(addResp.getData()), JsonObject.class); + JsonArray replies = jsObj.getAsJsonArray("replies"); + JsonObject jsonObject = replies.get(0).getAsJsonObject(); + // 使用已有的Reply类 + Reply reply = gson.fromJson(jsonObject, Reply.class); + sheetId = reply.getAddSheet().getProperties().getSheetId(); + if (sheetId == null || sheetId.isEmpty()) { + log.log(Level.SEVERE, "【飞书表格】 创建 sheet 失败!"); + throw new FsHelperException("【飞书表格】创建 sheet 异常!SheetId返回为空!"); + } + } else { + log.log(Level.SEVERE, "【飞书表格】 创建 sheet 失败!错误信息:{0}", gson.toJson(addResp)); + throw new FsHelperException("【飞书表格】 创建 sheet 异常!"); + } + } catch (Exception e) { + String message = e.getMessage(); + log.log(Level.SEVERE, "【飞书表格】 创建 sheet 异常!错误信息:{0}", message); + + throw new FsHelperException(message != null && message.contains("403")? "请按照上方操作,当前智投无法操作对应文档哦" : "【飞书表格】 创建 sheet 异常!"); + } + return sheetId; + } + + + public static String copySheet(String sourceSheetId, String title, FeishuClient client, String spreadsheetToken) { + String sheetId = null; + try { + CustomSheetService.SheetBatchUpdateRequest copyRequest = CustomSheetService.SheetBatchUpdateRequest.newBuilder() + .addRequest(CustomSheetService.SheetRequest.copySheet() + .sourceSheetId(sourceSheetId) + .destinationTitle(title) + .build()) + .build(); + + ApiResponse copyResp = client.customSheets().sheetsBatchUpdate(spreadsheetToken, copyRequest); + + if (copyResp.success()) { + log.log(Level.INFO, "【飞书表格】 复制 sheet 成功! {0}", gson.toJson(copyResp)); + + JsonObject jsObj = gson.fromJson(gson.toJson(copyResp.getData()), JsonObject.class); + JsonArray replies = jsObj.getAsJsonArray("replies"); + JsonObject jsonObject = replies.get(0).getAsJsonObject(); + // 使用已有的Reply类 + Reply reply = gson.fromJson(jsonObject, Reply.class); + CopySheet copySheet = reply.getCopySheet(); + sheetId = copySheet.getProperties().getSheetId(); + if (sheetId == null || sheetId.isEmpty()) { + throw new FsHelperException("【飞书表格】 复制模版异常!SheetID 为空!"); + } + } + } catch (Exception e) { + log.log(Level.SEVERE, "【飞书表格】 复制模版异常!错误信息:{0}", e.getMessage()); + throw new FsHelperException("【飞书表格】 复制模版异常!"); + } + return sheetId; + } + + public static void setDateType(String sheetId, FeishuClient client, String spreadsheetToken, String conf, Integer headLine) { + JsonObject confObj = gson.fromJson(conf, JsonObject.class); + String position = confObj.get("position").getAsString(); + String formatter = confObj.get("formatter").getAsString(); + if (position == null || position.trim().isEmpty()) return; + + try { + CustomCellService.CellBatchUpdateRequest batchStyleRequest = CustomCellService.CellBatchUpdateRequest.newBuilder() + .addRequest(CustomCellService.CellRequest.styleCells() + .sheetId(sheetId) + .startPosition(position + headLine) + .endPosition(position + DEFAULT_ROW_NUM) + .formatter(formatter) + .build()) + .build(); + + ApiResponse batchStyleResp = client.customCells().cellsBatchUpdate(spreadsheetToken, batchStyleRequest); + + if (!batchStyleResp.success()) { + log.log(Level.SEVERE, "【飞书表格】 写入表格样式数据异常!参数:{0},错误信息:{1}", new Object[]{conf, batchStyleResp.getMsg()}); + } + } catch (Exception e) { + log.log(Level.SEVERE, "【飞书表格】 写入表格样式数据异常!", e); + } + } + + public static void setOptions(String sheetId, FeishuClient client, String spreadsheetToken, boolean isMulti, + String startPosition, String endPosition, List result) { + + try { + // 创建设置下拉列表请求 + CustomDataValidationService.DataValidationRequest listRequest = CustomDataValidationService.DataValidationRequest.listValidation() + .range(sheetId, startPosition, endPosition) // 设置范围 + .addValues(result) // 添加下拉选项 + .multipleValues(isMulti) // 设置支持多选 + .build(); + + // 添加到批量请求中 + CustomDataValidationService.DataValidationBatchUpdateRequest batchRequest = CustomDataValidationService.DataValidationBatchUpdateRequest.newBuilder() + .addRequest(listRequest) + .build(); + + // 执行请求 + ApiResponse response = client.customDataValidations().dataValidationBatchUpdate(spreadsheetToken, batchRequest); + + if (!response.success()) { + log.log(Level.SEVERE, "设置下拉列表失败, sheetId:{0}, startPosition:{1}, endPosition: {2}, 返回信息:{3}", new Object[]{sheetId, startPosition, endPosition, gson.toJson(response)}); + } + } catch (Exception e) { + log.log(Level.SEVERE, "设置下拉列表失败,sheetId:{0}", new Object[]{sheetId}); + } + } + + public static void removeSheet(String sheetId, FeishuClient client, String spreadsheetToken) { + try { + CustomSheetService.SheetBatchUpdateRequest deleteRequest = CustomSheetService.SheetBatchUpdateRequest.newBuilder() + .addRequest(CustomSheetService.SheetRequest.deleteSheet() + .sheetId(sheetId) + .build()) + .build(); + + ApiResponse deleteResp = client.customSheets().sheetsBatchUpdate(spreadsheetToken, deleteRequest); + + if (!deleteResp.success()) { + log.log(Level.SEVERE, "【飞书表格】 删除 sheet 失败!参数:{0},错误信息:{1}", new Object[]{sheetId, deleteResp.getMsg()}); + } + } catch (Exception e) { + log.log(Level.SEVERE, "【飞书表格】 删除 sheet 异常!参数:{0},错误信息:{1}", new Object[]{sheetId, e.getMessage()}); + } + } + + /** + * 下载素材 + */ + public static void downloadMaterial(String fileToken, String outputPath, FeishuClient client, String extra) { + try { + DownloadMediaReq req = DownloadMediaReq.newBuilder() + .fileToken(fileToken) +// .extra("无") + .build(); + + // 发起请求 + DownloadMediaResp resp = client.drive().v1().media().download(req); + + // 处理服务端错误 + if (resp.success()) { + resp.writeFile(outputPath); + } + + } catch (Exception e) { + log.log(Level.SEVERE, "【飞书表格】 下载素材异常!参数:{0},错误信息:{1}", new Object[]{fileToken, e.getMessage()}); + throw new FsHelperException("【飞书表格】 下载素材异常!"); + } + } + + public static String downloadTmpMaterialUrl(String fileToken, FeishuClient client) { + String tmpUrl = ""; + try { + BatchGetTmpDownloadUrlMediaReq req = BatchGetTmpDownloadUrlMediaReq.newBuilder() + .fileTokens(new String[]{fileToken}) + .build(); + + BatchGetTmpDownloadUrlMediaResp resp = client.drive().v1().media().batchGetTmpDownloadUrl(req); + + if (resp.success()) { + return resp.getData().getTmpDownloadUrls()[0].getTmpDownloadUrl(); + } else { + log.log(Level.SEVERE, "【飞书表格】 获取临时下载地址失败!参数:{0},错误信息:{1}", new Object[]{fileToken, gson.toJson(resp)}); + } + } catch (Exception e) { + log.log(Level.SEVERE, "【飞书表格】 获取临时下载地址异常!参数:{0},错误信息:{1}", new Object[]{fileToken, e.getMessage()}); + } + return tmpUrl; + } + + public static Object putValues(String spreadsheetToken, CustomValueService.ValueRequest putValuesBuilder, FeishuClient client) { + log.log(Level.INFO, "【飞书表格】 putValues 开始写入数据!参数:{0}", gson.toJson(putValuesBuilder)); + + // 添加到批量请求中 + CustomValueService.ValueBatchUpdateRequest putDataRequest = CustomValueService.ValueBatchUpdateRequest.newBuilder() + .addRequest(putValuesBuilder) + .build(); + + try { + ApiResponse putResp = client.customValues().valueBatchUpdate(spreadsheetToken, putDataRequest); + if (putResp.success()) { + return putResp.getData(); + } else { + log.log(Level.SEVERE, "【飞书表格】 写入表格数据失败!参数:{0},错误信息:{1}", new Object[]{putValuesBuilder, putResp.getMsg()}); + throw new FsHelperException("【飞书表格】 写入表格数据失败!"); + } + } catch (IOException e) { + log.log(Level.SEVERE, "【飞书表格】 写入表格数据异常!参数:{0},错误信息:{1}", new Object[]{spreadsheetToken, e.getMessage()}); + throw new FsHelperException("【飞书表格】 写入表格数据异常!"); + } + } + + public static Object batchPutValues(String sheetId, String spreadsheetToken, + CustomValueService.ValueRequest batchPutRequest, FeishuClient client) { + + log.log(Level.INFO, "【飞书表格】 batchPutValues 开始写入数据!参数:{0}", gson.toJson(batchPutRequest)); + + try { + CustomValueService.ValueBatchUpdateRequest batchPutDataRequest = + CustomValueService.ValueBatchUpdateRequest.newBuilder() + .addRequest(batchPutRequest) + .build(); + + ApiResponse batchPutResp = client.customValues().valueBatchUpdate(spreadsheetToken, batchPutDataRequest); + if (batchPutResp.success()) { + return batchPutResp.getData(); + } else { + log.log(Level.SEVERE, "【飞书表格】 批量写入数据失败!参数:{0},错误信息:{1}", new Object[]{sheetId, gson.toJson(batchPutResp)}); + throw new FsHelperException("【飞书表格】 批量写入数据失败!"); + } + } catch (Exception e) { + log.log(Level.SEVERE, "【飞书表格】 批量写入数据异常!参数:{0},错误信息:{1}", new Object[]{sheetId, e.getMessage()}); + throw new FsHelperException("【飞书表格】 批量写入数据异常!"); + } + } + + public static Object addRowColumns(String sheetId, String spreadsheetToken, String type, int length,FeishuClient client) { + + CustomDimensionService.DimensionBatchUpdateRequest batchRequest = CustomDimensionService.DimensionBatchUpdateRequest.newBuilder() + .addRequest(CustomDimensionService.DimensionRequest.addDimension() + .sheetId(sheetId) + .majorDimension(type) + .length(length).build()) + .build(); + + try { + ApiResponse batchResp = client.customDimensions().dimensionsBatchUpdate(spreadsheetToken, batchRequest); + if (batchResp.success()) { + return batchResp.getData(); + } else { + log.log(Level.SEVERE, "【飞书表格】 添加行列失败!参数:{0},错误信息:{1}", new Object[]{sheetId, gson.toJson(batchResp)}); + throw new FsHelperException("【飞书表格】 添加行列失败!"); + } + } catch (IOException e) { + log.log(Level.SEVERE, "【飞书表格】 添加行列异常!参数:{0},错误信息:{1}", new Object[]{sheetId, e.getMessage()}); + throw new FsHelperException("【飞书表格】 添加行列异常!"); + } + } + + public static Object getTableInfo(String sheetId, String spreadsheetToken, FeishuClient client) { + try { + // 创建请求对象 + GetSpreadsheetReq req = GetSpreadsheetReq.newBuilder() + .spreadsheetToken(spreadsheetToken) + .build(); + + // 发起请求 + GetSpreadsheetResp resp = client.sheets().v3().spreadsheet().get(req); + + // 处理服务端错误 + if (resp.success()) { + return resp.getData(); + } else { + log.log(Level.SEVERE, "【飞书表格】 获取表格信息失败!参数:{0},错误信息:{1}", new Object[]{sheetId, resp.getMsg()}); + } + } catch (Exception e) { + log.log(Level.SEVERE, "【飞书表格】 获取表格信息异常!参数:{0},错误信息:{1}", new Object[]{sheetId, e.getMessage()}); + } + return null; + } + + /** + * 字符串类型: formatter: "@" + */ + public static void setCellType(String sheetId, String formatter, String startPosition, String endPosition, FeishuClient client, String spreadsheetToken) { + try { + CustomCellService.CellBatchUpdateRequest batchUpdateRequest = CustomCellService.CellBatchUpdateRequest.newBuilder() + .addRequest(CustomCellService.CellRequest.styleCells() + .formatter(formatter).sheetId(sheetId).startPosition(startPosition).endPosition(endPosition) + .build()) + .build(); + + ApiResponse apiResponse = client.customCells().cellsBatchUpdate(spreadsheetToken, batchUpdateRequest); + if (!apiResponse.success()) { + log.log(Level.SEVERE, "【飞书表格】 设置单元格类型失败!参数:{0},错误信息:{1}", new Object[]{sheetId, apiResponse.getMsg()}); + throw new FsHelperException("【飞书表格】 批量设置单元格类型失败!"); + } + } catch (Exception e) { + log.log(Level.SEVERE, "【飞书表格】 设置单元格类型失败!参数:{0},错误信息:{1}", new Object[]{sheetId, e.getMessage()}); + throw new FsHelperException("【飞书表格】 批量设置单元格类型异常!"); + } + } + + public static Object imageUpload(String filePath, String fileName, String position ,String sheetId, String spreadsheetToken, FeishuClient client) { + try { + byte[] imageData = FileUtil.getImageData(filePath); + + CustomValueService.ValueRequest imageRequest = CustomValueService.ValueRequest.imageValues() + .range(sheetId, position) + .image(imageData) + .name(fileName) + .build(); + + CustomValueService.ValueBatchUpdateRequest imageWriteRequest = CustomValueService.ValueBatchUpdateRequest.newBuilder() + .addRequest(imageRequest) + .build(); + + ApiResponse imageResp = client.customValues().valueBatchUpdate(spreadsheetToken, imageWriteRequest); + + if (!imageResp.success()) { + log.log(Level.SEVERE, "【飞书表格】 图片上传失败!参数:{0},错误信息:{1}", new Object[]{filePath, gson.toJson(imageResp)}); + } + return imageResp.getData(); + } catch (Exception e) { + log.log(Level.SEVERE, "【飞书表格】 图片上传异常!参数:{0},错误信息:{1}", new Object[]{filePath, e.getMessage()}); + } + + return null; + } +} \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/utils/FsClientUtil.java b/src/main/java/cn/isliu/core/utils/FsClientUtil.java new file mode 100644 index 0000000..0d8fe56 --- /dev/null +++ b/src/main/java/cn/isliu/core/utils/FsClientUtil.java @@ -0,0 +1,35 @@ +package cn.isliu.core.utils; + +import cn.isliu.core.client.FeishuClient; + + +public class FsClientUtil { + + public static FeishuClient client; + + /** + * 获取飞书客户端 + * + * @param appId 飞书应用ID + * @param appSecret 飞书应用密钥 + * @return 飞书客户端 + */ + public static FeishuClient initFeishuClient(String appId, String appSecret) { + client = FeishuClient.newBuilder(appId, appSecret).build(); + return client; + } + + /** + * 设置飞书客户端 + * + * @param appId 飞书应用ID + * @param appSecret 飞书应用密钥 + */ + public static void setClient(String appId, String appSecret) { + client = FeishuClient.newBuilder(appId, appSecret).build(); + } + + public static FeishuClient getFeishuClient() { + return client; + } +} diff --git a/src/main/java/cn/isliu/core/utils/FsTableUtil.java b/src/main/java/cn/isliu/core/utils/FsTableUtil.java new file mode 100644 index 0000000..f357540 --- /dev/null +++ b/src/main/java/cn/isliu/core/utils/FsTableUtil.java @@ -0,0 +1,323 @@ +package cn.isliu.core.utils; + +import cn.isliu.core.*; +import cn.isliu.core.annotation.TableProperty; +import cn.isliu.core.config.FsConfig; +import cn.isliu.core.converters.OptionsValueProcess; +import cn.isliu.core.enums.BaseEnum; +import cn.isliu.core.enums.TypeEnum; +import cn.isliu.core.pojo.FieldProperty; +import cn.isliu.core.service.CustomValueService; + +import java.lang.reflect.InvocationTargetException; +import java.util.*; +import java.util.stream.Collectors; + +public class FsTableUtil { + + public static List getFsTableData(Sheet sheet, String spreadsheetToken) { + + // 计算数据范围 + GridProperties gridProperties = sheet.getGridProperties(); + int totalRow = gridProperties.getRowCount(); + int rowCount = Math.min(totalRow, 100); // 每次读取的行数 + int colCount = gridProperties.getColumnCount(); + int startOffset = 1; // 起始偏移行号 + + // 实际要读取的数据行数(减去偏移量) + int actualRows = Math.max(0, totalRow - startOffset); + int batchCount = (actualRows + rowCount - 1) / rowCount; + + List> values = new LinkedList<>(); + for (int i = 0; i < batchCount; i++) { + int startRowIndex = startOffset + i * rowCount; + int endRowIndex = Math.max(startRowIndex + rowCount - 1, totalRow - 1); + + // 3. 获取工作表数据 + ValuesBatch valuesBatch = FsApiUtil.getSheetData(sheet.getSheetId(), spreadsheetToken, + "A" + startRowIndex, + getColumnName(colCount - 1) + endRowIndex, FsClientUtil.getFeishuClient()); + if (valuesBatch != null) { + List valueRanges = valuesBatch.getValueRanges(); + for (ValueRange valueRange : valueRanges) { + values.addAll(valueRange.getValues()); + } + } + } + + // 获取飞书表格数据 + TableData tableData = processSheetData(sheet, values); + + List dataList = getFsTableData(tableData); + Map titleMap = new HashMap<>(); + + dataList.stream().filter(d -> d.getRow() == (FsConfig.getTitleLine() - 1)).findFirst() + .ifPresent(d -> { + Map map = (Map) d.getData(); + titleMap.putAll(map); + }); + return dataList.stream().filter(fsTableData -> fsTableData.getRow() >= FsConfig.getHeadLine()).map(item -> { + Map resultMap = new HashMap<>(); + + Map map = (Map) item.getData(); + map.forEach((k, v) -> { + String title = titleMap.get(k); + if (title != null) { + resultMap.put(title, v); + } + }); + item.setData(resultMap); + return item; + }).collect(Collectors.toList()); + } + + private static List getFsTableData(TableData tableData) { + return getFsTableData(tableData, new ArrayList<>()); + } + + private static List getFsTableData(TableData tableData, List ignoreUniqueFields) { + + List fsTableList = new LinkedList<>(); + // 5. 访问补齐后的数据 + for (TableRow row : tableData.getRows()) { + + FsTableData fsTableData = new FsTableData(); + int rowIndex = 0; + Map obj = new HashMap<>(); + for (Cell cell : row.getCells()) { + obj.put(getColumnName(cell.getCol()), cell.getValue()); + rowIndex = cell.getRow(); + } + + fsTableData.setRow(rowIndex); + fsTableData.setData(obj); + + String jsonStr; + if (!ignoreUniqueFields.isEmpty()) { + Map clone = new HashMap<>(obj); + ignoreUniqueFields.forEach(clone::remove); + jsonStr = StringUtil.mapToJson(clone); + } else { + jsonStr = StringUtil.mapToJson(obj); + } + + String uniqueId = StringUtil.getSHA256(jsonStr); + + fsTableData.setUniqueId(uniqueId); + fsTableList.add(fsTableData); + } + return fsTableList; + + } + + /** + * 处理表格数据,将合并单元格转换为对象,并补齐合并区域的值 + */ + public static TableData processSheetData(Sheet metadata, List> values) { + TableData tableData = new TableData(); + + // 创建单元格网格 + int rowCount = values.size(); + int colCount = values.stream().mapToInt(List::size).max().orElse(0); + Cell[][] grid = new Cell[rowCount][colCount]; + + // 1. 初始化网格 + for (int i = 0; i < rowCount; i++) { + List row = values.get(i); + for (int j = 0; j < colCount; j++) { + Object value = (j < row.size()) ? row.get(j) : null; + grid[i][j] = new Cell(i, j, value); + } + } + + // 2. 标记合并区域并补齐所有合并单元格的值 + if (metadata.getMerges() != null) { + for (Merge merge : metadata.getMerges()) { + int startRow = merge.getStartRowIndex(); + int endRow = merge.getEndRowIndex(); + int startCol = merge.getStartColumnIndex(); + int endCol = merge.getEndColumnIndex(); + + // 获取合并区域左上角的值 + Object topLeftValue = null; + if (startRow < rowCount && startCol < colCount) { + topLeftValue = grid[startRow][startCol].getValue(); + } + + // 遍历合并区域 + for (int i = startRow; i <= endRow; i++) { + for (int j = startCol; j <= endCol; j++) { + if (i < rowCount && j < colCount) { + // 标记合并区域 + grid[i][j].setMerge(merge); + + // 对于合并区域内除左上角外的所有单元格 + if (i != startRow || j != startCol) { + // 补齐值 + grid[i][j].setValue(topLeftValue); + } + } + } + } + } + } + + // 3. 构建表格数据结构 + for (int i = 0; i < rowCount; i++) { + // 检查整行是否都为null + boolean allNull = true; + for (int j = 0; j < colCount; j++) { + if (grid[i][j].getValue() != null) { + allNull = false; + break; + } + } + + // 如果整行都为null,跳过该行 + if (allNull) { + continue; + } + + TableRow tableRow = new TableRow(); + + for (int j = 0; j < colCount; j++) { + Cell cell = grid[i][j]; + + // 如果是合并区域的左上角 + if (cell.getMerge() != null && + cell.getRow() == cell.getMerge().getStartRowIndex() && + cell.getCol() == cell.getMerge().getStartColumnIndex()) { + + MergedCell mergedCell = new MergedCell(); + mergedCell.setValue(cell.getValue()); + mergedCell.setRow(cell.getRow()); + mergedCell.setCol(cell.getCol()); + mergedCell.setRowSpan(cell.getMerge().getRowSpan()); + mergedCell.setColSpan(cell.getMerge().getColSpan()); + + tableRow.getCells().add(mergedCell); + } else { + // 普通单元格或合并区域内的其他单元格 + tableRow.getCells().add(cell); + } + } + + tableData.getRows().add(tableRow); + } + + return tableData; + } + + + public static String getColumnName(int columnIndex) { + StringBuilder sb = new StringBuilder(); + while (columnIndex >= 0) { + char c = (char) ('A' + (columnIndex % 26)); + sb.insert(0, c); + columnIndex = (columnIndex / 26) - 1; + if (columnIndex < 0) break; + } + return sb.toString(); + } + + public static String getColumnNameByNuNumber(int columnNumber) { + StringBuilder columnName = new StringBuilder(); + while (columnNumber > 0) { + int remainder = (columnNumber - 1) % 26; + columnName.insert(0, (char) ('A' + remainder)); + columnNumber = (columnNumber - 1) / 26; + } + return columnName.toString(); + } + + public static Map getTitlePostionMap(Sheet sheet, String spreadsheetToken) { + GridProperties gridProperties = sheet.getGridProperties(); + int colCount = gridProperties.getColumnCount(); + + Map resultMap = new TreeMap<>(); + ValuesBatch valuesBatch = FsApiUtil.getSheetData(sheet.getSheetId(), spreadsheetToken, + "A" + FsConfig.getTitleLine(), + getColumnName(colCount - 1) + FsConfig.getTitleLine(), FsClientUtil.getFeishuClient()); + if (valuesBatch != null) { + List valueRanges = valuesBatch.getValueRanges(); + if (valueRanges != null && !valueRanges.isEmpty()) { + List values = valueRanges.get(0).getValues().get(0); + if (values != null && !values.isEmpty()) { + for (int i = 0; i < values.size(); i++) { + Object valObj = values.get(i); + if (valObj == null) { + continue; + } + String value = (String) valObj; + resultMap.put(value.trim(), getColumnName(i)); + } + } + } + } + + return resultMap; + } + + public static void setTableOptions(String spreadsheetToken, List headers, Map fieldsMap, String sheetId) { + List list = Arrays.asList(headers.toArray()); + fieldsMap.forEach((field, fieldProperty) -> { + TableProperty tableProperty = fieldProperty.getTableProperty(); + String position = ""; + if (tableProperty != null) { + for (int i = 0; i < list.size(); i++) { + Object obj = list.get(i); + if (obj.toString().equals(field)) { + position = FsTableUtil.getColumnNameByNuNumber(i + 1); + } + } + int line = FsConfig.getTitleLine() + 1; + + if (tableProperty.enumClass() != BaseEnum.class) { + FsApiUtil.setOptions(sheetId, FsClientUtil.getFeishuClient(), spreadsheetToken, tableProperty.type() == TypeEnum.MULTI_SELECT, position + line, position + 200, + Arrays.stream(tableProperty.enumClass().getEnumConstants()).map(BaseEnum::getDesc).collect(Collectors.toList())); + } + + if (tableProperty.optionsClass() != OptionsValueProcess.class) { + List result; + Class optionsClass = tableProperty.optionsClass(); + try { + OptionsValueProcess optionsValueProcess = optionsClass.getDeclaredConstructor().newInstance(); + result = (List) optionsValueProcess.process(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + + FsApiUtil.setOptions(sheetId, FsClientUtil.getFeishuClient(), spreadsheetToken, tableProperty.type() == TypeEnum.MULTI_SELECT, position + line, position + 200, + result); + } + } + }); + } + + public static CustomValueService.ValueRequest getHeadTemplateBuilder(String sheetId, List headers) { + CustomValueService.ValueRequest.BatchPutValuesBuilder batchPutValuesBuilder = CustomValueService.ValueRequest.batchPutValues(); + batchPutValuesBuilder.addRange(sheetId + "!A1:" + FsTableUtil.getColumnNameByNuNumber(headers.size()) + "1"); + batchPutValuesBuilder.addRow(headers.toArray()); + return batchPutValuesBuilder.build(); + } + + public static String getDefaultTableStyle(String sheetId, int size) { + String colorTemplate = "{\"data\": [{\"style\": {\"font\": {\"bold\": true, \"clean\": false, \"italic\": false, \"fontSize\": \"10pt/1.5\"}, \"clean\": false, \"hAlign\": 1, \"vAlign\": 1, \"backColor\": \"#000000\", \"foreColor\": \"#ffffff\", \"formatter\": \"\", \"borderType\": \"FULL_BORDER\", \"borderColor\": \"#000000\", \"textDecoration\": 0}, \"ranges\": [\"SHEET_ID!RANG\"]}]}"; + colorTemplate = colorTemplate.replace("SHEET_ID", sheetId); + colorTemplate = colorTemplate.replace("RANG", "A1:" + FsTableUtil.getColumnNameByNuNumber(size) + "1"); + colorTemplate = colorTemplate.replace("FORE_COLOR", FsConfig.FORE_COLOR).replace("BACK_COLOR", FsConfig.BACK_COLOR); + return colorTemplate; + } + + public static void main(String[] args) { + FsConfig.initConfig("cli_a73813628afbd00d", "ouFTDr0Qu5WCgoPS8mgULg6uT0lDEUtX"); + + String sheetId = "2HokFi"; + String spreadsheetToken = "SYRysUIcaheEbNt8KTocxRBinCh"; + Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, FsClientUtil.getFeishuClient(), spreadsheetToken); + + + Map headFieldMap = getTitlePostionMap(sheet, spreadsheetToken); + System.out.println(headFieldMap); + } +} \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/utils/GenerateUtil.java b/src/main/java/cn/isliu/core/utils/GenerateUtil.java new file mode 100644 index 0000000..148806a --- /dev/null +++ b/src/main/java/cn/isliu/core/utils/GenerateUtil.java @@ -0,0 +1,434 @@ +package cn.isliu.core.utils; + +import cn.isliu.core.annotation.TableProperty; +import cn.isliu.core.converters.FieldValueProcess; +import cn.isliu.core.enums.BaseEnum; +import cn.isliu.core.utils.StringUtil; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +public class GenerateUtil { + + private static final Logger log = Logger.getLogger(GenerateUtil.class.getName()); + + /** + * 根据配置和数据生成DTO对象(通用版本) + */ + public static T generateInstance(List fieldPathList, Class clazz, Map dataMap) { + T t; + try { + t = clazz.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new RuntimeException("创建实例失败: " + clazz.getSimpleName(), e); + } + + fieldPathList.forEach(fieldPath -> { + Object value = dataMap.get(fieldPath); + + if (value != null) { + try { + setNestedField(t, fieldPath, value); + } catch (Exception e) { + log.log(Level.SEVERE, "【巨量广告助手】 获取字段值异常!参数:{0},异常:{1}", new Object[]{fieldPath, e.getMessage()}); + } + } + }); + + return t; + } + + /** + * 递归设置嵌套字段值(支持List类型处理) + */ + private static void setNestedField(Object target, String fieldPath, Object value) + throws Exception { + String[] parts = fieldPath.split("\\."); + setNestedFieldRecursive(target, parts, 0, value); + } + + private static void setNestedFieldRecursive(Object target, String[] parts, int index, Object value) + throws Exception { + if (index >= parts.length - 1) { + // 最后一级字段,直接设置值 + setFieldValue(target, parts[index], value); + return; + } + + String fieldName = parts[index]; + Field field = getDeclaredField(target.getClass(), fieldName); + if (field == null) { + // 尝试使用下划线转驼峰的方式查找字段 + field = getDeclaredField(target.getClass(), StringUtil.toCamelCase(fieldName)); + if (field == null) { + throw new NoSuchFieldException("Field not found: " + fieldName); + } + } + + field.setAccessible(true); + Object nestedObj = field.get(target); + + // 处理List类型 + if (List.class.isAssignableFrom(field.getType())) { + // 确保List存在 + if (nestedObj == null) { + nestedObj = new ArrayList<>(); + field.set(target, nestedObj); + } + + List list = (List) nestedObj; + // 确保List中至少有一个元素 + if (list.isEmpty()) { + Object newElement = createListElement(field); + list.add(newElement); + } + + // 使用List中的第一个元素 + nestedObj = list.get(0); + } + // 处理普通对象 + else { + // 确保嵌套对象存在 + if (nestedObj == null) { + // 通过反射创建嵌套对象实例 + try { + nestedObj = field.getType().getDeclaredConstructor().newInstance(); + field.set(target, nestedObj); + } catch (Exception e) { + // 如果无法创建实例,则记录日志并跳过该字段 + log.log(Level.WARNING, "无法创建嵌套对象实例: " + field.getType().getName() + ", 字段: " + fieldName, e); + return; + } + } + } + + // 递归处理下一级 + setNestedFieldRecursive(nestedObj, parts, index + 1, value); + } + + /** + * 创建List元素实例 + */ + private static Object createListElement(Field listField) throws Exception { + Type genericType = listField.getGenericType(); + if (genericType instanceof ParameterizedType) { + Type[] typeArgs = ((ParameterizedType) genericType).getActualTypeArguments(); + if (typeArgs.length > 0 && typeArgs[0] instanceof Class) { + Class elementClass = (Class) typeArgs[0]; + // 通过反射创建List元素实例 + try { + return elementClass.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + // 如果无法创建实例,则记录日志并返回null + log.log(Level.WARNING, "无法创建List元素实例: " + elementClass.getName(), e); + return null; + } + } + } + throw new InstantiationException("Cannot determine list element type for field: " + listField.getName()); + } + + /** + * 设置字段值(支持基本类型转换) + */ + private static void setFieldValue(Object target, String fieldName, Object value) + throws Exception { + Field field = getDeclaredField(target.getClass(), fieldName); + if (field == null) { + // 尝试使用下划线转驼峰的方式查找字段 + field = getDeclaredField(target.getClass(), StringUtil.toCamelCase(fieldName)); + if (field == null) { + throw new NoSuchFieldException("Field not found: " + fieldName); + } + } + + field.setAccessible(true); + Class fieldType = field.getType(); + + // 简单类型转换 + if (value != null) { + if (fieldType == String.class) { + field.set(target, convertStrValue(value)); + } else if (fieldType == Integer.class || fieldType == int.class) { + field.set(target, Integer.parseInt(convertValue(value))); + } else if (fieldType == Double.class || fieldType == double.class) { + field.set(target, Double.parseDouble(convertValue(value))); + } else if (fieldType == Boolean.class || fieldType == boolean.class) { + field.set(target, Boolean.parseBoolean(convertValue(value))); + } else if (fieldType == Long.class || fieldType == long.class) { + String stringValue = convertValue(value); + try { + field.set(target, Long.parseLong(stringValue)); + } catch (NumberFormatException e) { + try { + field.set(target, ((Double) Double.parseDouble(stringValue)).longValue()); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException("无法将值 '" + stringValue + "' 转换为 long 或科学计数法表示的数值", ex); + } + } + + } else if (fieldType == List.class) { + // 获取泛型类型 + Type genericType = field.getGenericType(); + if (!(genericType instanceof ParameterizedType)) { + throw new IllegalArgumentException("无法获取字段的泛型信息:" + fieldName); + } + + Type elementType = ((ParameterizedType) genericType).getActualTypeArguments()[0]; + if (!(elementType instanceof Class)) { + throw new IllegalArgumentException("不支持非Class类型的泛型:" + elementType); + } + + Class elementClass = (Class) elementType; + + List convertedList = new ArrayList<>(); + + if (value instanceof List) { + for (Object item : (List) value) { + convertedList.add(convertValue(item, elementClass)); + } + } else { + // 单个值转为单元素List,并做类型转换 + convertedList.add(convertValue(value, elementClass)); + } + + field.set(target, convertedList); + } // 新增:枚举类型支持 + else if (BaseEnum.class.isAssignableFrom(fieldType)) { + if (value instanceof String) { + field.set(target, parseEnum((Class) fieldType, (String) value)); + } + } + else { + // 其他类型直接设置 + field.set(target, value); + } + } else { + field.set(target, null); + } + } + + /** + * 获取字段(包括父类) + */ + private static Field getDeclaredField(Class clazz, String fieldName) { + // 首先尝试直接查找字段(驼峰命名) + for (Class c = clazz; c != null; c = c.getSuperclass()) { + try { + return c.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + // 继续在父类中查找 + } + } + + // 如果直接查找失败,尝试使用下划线转驼峰的方式查找字段 + String camelCaseFieldName = StringUtil.toCamelCase(fieldName); + for (Class c = clazz; c != null; c = c.getSuperclass()) { + try { + return c.getDeclaredField(camelCaseFieldName); + } catch (NoSuchFieldException e) { + // 继续在父类中查找 + } + } + + return null; + } + + private static String convertStrValue(Object value) { + String result = ""; + if (value instanceof BigDecimal) { + BigDecimal bigDecimal = (BigDecimal) value; + result = bigDecimal.stripTrailingZeros().toPlainString(); + } else if (value instanceof Double) { + String stringValue = convertValue(value); + try { + result = String.valueOf(Long.parseLong(stringValue)); + } catch (NumberFormatException e) { + try { + result = String.valueOf(((Double) Double.parseDouble(stringValue)).longValue()); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException("无法将值 '" + stringValue + "' 转换为 long 或科学计数法表示的数值", ex); + } + } + } else { + result = String.valueOf(value); + } + return result.trim(); + } + + private static String convertValue(Object value) { + String result = ""; + if (value instanceof BigDecimal) { + BigDecimal bigDecimal = (BigDecimal) value; + result = bigDecimal.stripTrailingZeros().toPlainString(); + } else { + result = String.valueOf(value); + } + return result.trim(); + } + + private static Object convertValue(Object value, Class targetType) throws Exception { + if (value == null) return null; + + if (targetType.isAssignableFrom(value.getClass())) { + return value; + } + + if (targetType == String.class) { + return value.toString().trim(); + } else if (targetType == Integer.class || targetType == int.class) { + return Integer.parseInt(value.toString().trim()); + } else if (targetType == Double.class || targetType == double.class) { + return Double.parseDouble(value.toString().trim()); + } else if (targetType == Long.class || targetType == long.class) { + return Long.parseLong(value.toString().trim()); + } else if (BaseEnum.class.isAssignableFrom(targetType)) { + if (value instanceof String) { + return parseEnum((Class) targetType, (String) value); + } else { + throw new IllegalArgumentException("枚举类型字段只接受字符串类型的值"); + } + } else { + throw new IllegalArgumentException("不支持的List元素类型转换:" + targetType.getName()); + } + } + + public static T parseEnum(Class enumClass, String value) { + String val = value.trim(); + return Arrays.stream(enumClass.getEnumConstants()) + .filter(e -> e.getCode().equals(val) || e.getDesc().equals(val)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No matching enum for value: " + val)); + } + + /** + * 根据实体类以及字段映射关系,获取该字段的值 field 为:"field.field2.field3" + * + * @param target 实体类对象 + * @param fieldMap 字段映射关系,key为注解值,value为实际字段路径 + * @return 字段值 + */ + public static Map getFieldValue(Object target, Map fieldMap) { + // 遍历字段映射关系,获取字段值 + Map result = new HashMap<>(); + for (Map.Entry entry : fieldMap.entrySet()) { + String fieldName = entry.getKey(); + String fieldPath = entry.getValue(); + + try { + Object value = getNestedFieldValue(target, fieldPath); + if (value != null) { + result.put(fieldName, value); + } + } catch (Exception e) { + log.log(Level.WARNING, "获取字段值异常,字段路径:" + fieldPath, e); + } + } + return result; + } + + /** + * 递归获取嵌套字段值(支持List类型处理) + */ + public static Object getNestedFieldValue(Object target, String fieldPath) throws Exception { + String[] parts = fieldPath.split("\\."); + return getNestedFieldValueRecursive(target, parts, 0); + } + + private static Object getNestedFieldValueRecursive(Object target, String[] parts, int index) throws Exception { + if (target == null) { + return null; + } + + if (index >= parts.length - 1) { + // 最后一级字段,直接获取值 + return getFieldValue(target, parts[index]); + } + + String fieldName = parts[index]; + Field field = getDeclaredField(target.getClass(), fieldName); + if (field == null) { + // 尝试使用下划线转驼峰的方式查找字段 + field = getDeclaredField(target.getClass(), StringUtil.toCamelCase(fieldName)); + if (field == null) { + throw new NoSuchFieldException("Field not found: " + fieldName); + } + } + + field.setAccessible(true); + Object nestedObj = field.get(target); + + // 处理List类型 + if (nestedObj instanceof List) { + List list = (List) nestedObj; + if (!list.isEmpty()) { + // 使用List中的第一个元素 + nestedObj = list.get(0); + } else { + return null; + } + } + + // 递归处理下一级 + return getNestedFieldValueRecursive(nestedObj, parts, index + 1); + } + + /** + * 获取字段值 + */ + private static Object getFieldValue(Object target, String fieldName) throws Exception { + Field field = getDeclaredField(target.getClass(), fieldName); + if (field == null) { + // 尝试使用下划线转驼峰的方式查找字段 + field = getDeclaredField(target.getClass(), StringUtil.toCamelCase(fieldName)); + if (field == null) { + throw new NoSuchFieldException("Field not found: " + fieldName); + } + } + + field.setAccessible(true); + Object newObject = field.get(target); + // 处理List类型 + if (newObject instanceof List) { + List list = (List) newObject; + if (!list.isEmpty()) { + Field finalField = field; + newObject = list.stream().map(obj -> ConvertFieldUtil.reverseValueConversion(finalField.getAnnotation(TableProperty.class), obj)) + .collect(Collectors.toList()); + } else { + return null; + } + } else { + newObject = ConvertFieldUtil.reverseValueConversion(field.getAnnotation(TableProperty.class), newObject); + } + return newObject; + } + + public static Object getFieldValueList(Object fieldValue) { + Map params = new HashMap<>(); + params.put("values", fieldValue); + params.put("type", "multipleValue"); + return params; + } + + public static @Nullable String getUniqueId(T data) { + String uniqueId = null; + try { + Object uniqueIdObj = GenerateUtil.getNestedFieldValue(data, "uniqueId"); + if (uniqueIdObj != null) { + uniqueId = uniqueIdObj.toString(); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + return uniqueId; + } + +} \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/utils/JSONUtil.java b/src/main/java/cn/isliu/core/utils/JSONUtil.java new file mode 100644 index 0000000..0ec9c30 --- /dev/null +++ b/src/main/java/cn/isliu/core/utils/JSONUtil.java @@ -0,0 +1,49 @@ +package cn.isliu.core.utils; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import java.util.HashMap; +import java.util.Map; + +public class JSONUtil { + + private static final Gson gson = new Gson(); + + /** + * 手动将HashMap转换为JsonObject,避免Gson添加额外引号 + * @param data HashMap数据 + * @return 转换后的JsonObject + */ + public static JsonObject convertHashMapToJsonObject(HashMap data) { + JsonObject jsonObject = new JsonObject(); + for (Map.Entry entry : data.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + // 根据值的类型添加到JsonObject中 + if (value instanceof String) { + // 检查字符串是否已经包含引号,如果是则去除 + String strValue = (String) value; + if (strValue.length() >= 2 && strValue.startsWith("\"") && strValue.endsWith("\"")) { + strValue = strValue.substring(1, strValue.length() - 1); + } + jsonObject.addProperty(key, strValue); + } else if (value instanceof Number) { + jsonObject.addProperty(key, (Number) value); + } else if (value instanceof Boolean) { + jsonObject.addProperty(key, (Boolean) value); + } else if (value instanceof Character) { + jsonObject.addProperty(key, (Character) value); + } else if (value == null) { + jsonObject.add(key, null); + } else { + // 对于其他类型,使用Gson转换 + JsonElement element = gson.toJsonTree(value); + jsonObject.add(key, element); + } + } + return jsonObject; + } +} diff --git a/src/main/java/cn/isliu/core/utils/PropertyUtil.java b/src/main/java/cn/isliu/core/utils/PropertyUtil.java new file mode 100644 index 0000000..265c206 --- /dev/null +++ b/src/main/java/cn/isliu/core/utils/PropertyUtil.java @@ -0,0 +1,336 @@ +package cn.isliu.core.utils; + +import cn.isliu.core.annotation.TableProperty; +import cn.isliu.core.converters.FieldValueProcess; +import cn.isliu.core.converters.OptionsValueProcess; +import cn.isliu.core.enums.BaseEnum; +import cn.isliu.core.enums.TypeEnum; +import cn.isliu.core.pojo.FieldProperty; +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.*; +import java.util.stream.Collectors; + +public class PropertyUtil { + + /** + * 获取类及其嵌套类上@TableProperty注解的字段映射关系 + * 注解中的值作为key,FieldProperty对象作为value + * + * @param clazz 要处理的类 + * @return 包含所有@TableProperty注解字段映射关系的Map,嵌套属性使用'.'连接 + */ + public static Map getTablePropertyFieldsMap(Class clazz) { + Map allFields = new TreeMap<>(); + Map fieldsWithChildren = new TreeMap<>(); + getTablePropertyFieldsMapRecursive(clazz, allFields, "", "", new HashMap<>(), fieldsWithChildren, false); + + // 过滤掉有子级的字段 + Map result = new HashMap<>(); + for (Map.Entry entry : allFields.entrySet()) { + if (!fieldsWithChildren.containsKey(entry.getKey())) { + result.put(entry.getKey(), entry.getValue()); + } + } + return result; + } + + /** + * 递归获取类及其嵌套类上@TableProperty注解的字段映射关系 + * + * @param clazz 当前处理的类 + * @param result 存储结果的Map + * @param keyPrefix key的前缀(使用注解中的值构建) + * @param valuePrefix value的前缀(使用字段实际名称构建) + * @param depthMap 记录每个类的递归深度,用于检测循环引用 + * @param fieldsWithChildren 记录有子级的字段集合 + * @param parentHasAnnotation 父节点是否有注解 + */ + private static void getTablePropertyFieldsMapRecursive(Class clazz, Map result, String keyPrefix, String valuePrefix, Map, Integer> depthMap, Map fieldsWithChildren, boolean parentHasAnnotation) { + // 检查类是否在允许的包范围内 + if (!isTargetPackageClass(clazz)) { + return; + } + + // 检测循环引用,限制递归深度 + Integer currentDepth = depthMap.getOrDefault(clazz, 0); + if (currentDepth > 5) { // 限制最大递归深度为5 + // 遇到可能的循环引用时,只添加当前字段路径 + if (!keyPrefix.isEmpty()) { + TableProperty dummyAnnotation = createDummyTableProperty(valuePrefix); + result.put(keyPrefix, new FieldProperty(valuePrefix, dummyAnnotation)); + } + return; + } + + // 增加当前类的递归深度 + depthMap.put(clazz, currentDepth + 1); + + try { + // 获取所有声明的字段 + Field[] fields = clazz.getDeclaredFields(); + + for (Field field : fields) { + // 检查字段是否有@TableProperty注解 + if (field.isAnnotationPresent(TableProperty.class)) { + TableProperty tableProperty = field.getAnnotation(TableProperty.class); + String[] propertyValues = tableProperty.value().split("\\."); // 支持多个值 + String propertyValue = (propertyValues.length > 0 && !propertyValues[0].isEmpty()) ? propertyValues[0] : field.getName(); + + processFieldForMap(field, propertyValue, keyPrefix, valuePrefix, result, depthMap, fieldsWithChildren, parentHasAnnotation); + } else { + // 即使字段没有@TableProperty注解,也要递归处理复杂类型字段 + // 这样可以确保子节点有注解的字段也能被获取到 + processFieldWithoutAnnotation(field, keyPrefix, valuePrefix, result, depthMap, fieldsWithChildren, parentHasAnnotation); + } + } + } finally { + // 减少当前类的递归深度 + if (currentDepth == 0) { + depthMap.remove(clazz); + } else { + depthMap.put(clazz, currentDepth); + } + } + } + + /** + * 判断类是否在目标包范围内 + * 只处理用户定义的类,避免处理系统类如java.lang.ref.ReferenceQueue + * + * @param clazz 要检查的类 + * @return 是否为目标包下的类 + */ + private static boolean isTargetPackageClass(Class clazz) { + if (clazz == null) { + return false; + } + + String className = clazz.getName(); + // 只处理用户自定义的类,排除系统类 + return !className.startsWith("java.") && + !className.startsWith("javax.") && + !className.startsWith("sun.") && + !className.startsWith("com.sun.") && + !className.startsWith("jdk."); + } + + /** + * 创建一个虚拟的TableProperty注解实例 + * + * @param value 注解值 + * @return TableProperty实例 + */ + private static TableProperty createDummyTableProperty(String value) { + return new TableProperty() { + @Override + public Class annotationType() { + return TableProperty.class; + } + + @Override + public String value() { + return value; + } + + @Override + public String field() { + return ""; + } + + @Override + public int order() { + return Integer.MAX_VALUE; + } + + @Override + public TypeEnum type() { + return TypeEnum.TEXT; + } + + @Override + public Class enumClass() { + return BaseEnum.class; + } + + @Override + public Class fieldFormatClass() { + return FieldValueProcess.class; + } + + @Override + public Class optionsClass() { + return OptionsValueProcess.class; + } + }; + } + + /** + * 处理带有@TableProperty注解的字段 + * + * @param field 当前处理的字段 + * @param fieldPropertyName 字段名称(注解中的值) + * @param keyPrefix key的前缀(使用注解中的值构建) + * @param valuePrefix value的前缀(使用字段实际名称构建) + * @param result 存储结果的Map + * @param depthMap 记录每个类的递归深度,用于检测循环引用 + * @param fieldsWithChildren 有子级的字段集合 + * @param parentHasAnnotation 父节点是否有注解 + */ + private static void processFieldForMap(Field field, String fieldPropertyName, String keyPrefix, String valuePrefix, Map result, Map, Integer> depthMap, Map fieldsWithChildren, boolean parentHasAnnotation) { + // Key使用注解值构建(显示给用户的名称) + String fullKey = keyPrefix.isEmpty() ? fieldPropertyName : keyPrefix + "." + fieldPropertyName; + // Value使用字段实际名称构建(实际的代码字段路径) + String fullValue = valuePrefix.isEmpty() ? field.getName() : valuePrefix + "." + field.getName(); + TableProperty tableProperty = field.getAnnotation(TableProperty.class); + + // 如果字段是复杂类型,则递归处理 + Class fieldType = field.getType(); + if (isComplexType(fieldType)) { + // 用于收集子字段 + Map subFields = new HashMap<>(); + Map subFieldsWithChildren = new HashMap<>(); + + if (Collection.class.isAssignableFrom(fieldType)) { + // 处理集合类型,获取泛型参数类型 + Type genericType = field.getGenericType(); + if (genericType instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) genericType; + Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); + if (actualTypeArguments.length > 0 && actualTypeArguments[0] instanceof Class) { + Class elementType = (Class) actualTypeArguments[0]; + // 对于集合中的元素类型,递归处理其@TableProperty注解 + // 父节点有注解,所以子节点需要拼接 + getTablePropertyFieldsMapRecursive(elementType, subFields, fullKey, fullValue, depthMap, subFieldsWithChildren, true); + } + } + } else { + // 处理普通复杂类型 + // 父节点有注解,所以子节点需要拼接 + getTablePropertyFieldsMapRecursive(fieldType, subFields, fullKey, fullValue, depthMap, subFieldsWithChildren, true); + } + + // 将子字段添加到结果中 + result.putAll(subFields); + + // 如果有子字段,则标记当前字段为有子级(需要排除) + if (!subFields.isEmpty()) { + fieldsWithChildren.put(fullKey, fullValue); + // 同时将子字段中需要排除的也添加到当前需要排除的集合中 + fieldsWithChildren.putAll(subFieldsWithChildren); + } else { + // 如果没有子字段(如循环引用字段),则直接添加该字段 + result.put(fullKey, new FieldProperty(fullValue, tableProperty)); + } + } else { + // 简单类型直接添加 + result.put(fullKey, new FieldProperty(fullValue, tableProperty)); + } + } + + /** + * 处理没有@TableProperty注解的字段 + * 但仍需要递归处理复杂类型字段以确保子节点有注解的字段能被获取到 + * + * @param field 当前处理的字段 + * @param keyPrefix key的前缀(使用注解中的值构建) + * @param valuePrefix value的前缀(使用字段实际名称构建) + * @param result 存储结果的Map + * @param depthMap 记录每个类的递归深度,用于检测循环引用 + * @param fieldsWithChildren 有子级的字段集合 + * @param parentHasAnnotation 父节点是否有注解 + */ + private static void processFieldWithoutAnnotation(Field field, String keyPrefix, String valuePrefix, Map result, Map, Integer> depthMap, Map fieldsWithChildren, boolean parentHasAnnotation) { + // 即使字段没有注解,也要处理复杂类型字段 + Class fieldType = field.getType(); + if (isComplexType(fieldType)) { + // 构建新的前缀 + String newKeyPrefix; + String newValuePrefix = valuePrefix.isEmpty() ? field.getName() : valuePrefix + "." + field.getName(); + + // 关键修改:如果父节点没有注解,则不拼接父节点字段名 + if (parentHasAnnotation) { + // 父节点有注解,需要拼接 + newKeyPrefix = keyPrefix.isEmpty() ? field.getName() : keyPrefix + "." + field.getName(); + } else { + // 父节点没有注解,不拼接,保持空字符串或使用子节点自己的key + newKeyPrefix = ""; + } + + // 用于收集子字段 + Map subFields = new HashMap<>(); + Map subFieldsWithChildren = new HashMap<>(); + + if (Collection.class.isAssignableFrom(fieldType)) { + // 处理集合类型,获取泛型参数类型 + Type genericType = field.getGenericType(); + if (genericType instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) genericType; + Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); + if (actualTypeArguments.length > 0 && actualTypeArguments[0] instanceof Class) { + Class elementType = (Class) actualTypeArguments[0]; + // 对于集合中的元素类型,递归处理其@TableProperty注解 + // 父节点没有注解,所以子节点不需要拼接 + getTablePropertyFieldsMapRecursive(elementType, subFields, newKeyPrefix, newValuePrefix, depthMap, subFieldsWithChildren, false); + } + } + } else { + // 处理普通复杂类型 + // 父节点没有注解,所以子节点不需要拼接 + getTablePropertyFieldsMapRecursive(fieldType, subFields, newKeyPrefix, newValuePrefix, depthMap, subFieldsWithChildren, false); + } + + // 将子字段添加到结果中 + result.putAll(subFields); + + // 如果有子字段,则标记当前字段路径为有子级(需要排除) + if (!subFields.isEmpty()) { + // 注意:这里我们不使用注解值作为key,因为当前字段没有注解 + String currentPath = keyPrefix.isEmpty() ? field.getName() : keyPrefix + "." + field.getName(); + fieldsWithChildren.put(currentPath, newValuePrefix); + // 同时将子字段中需要排除的也添加到当前需要排除的集合中 + fieldsWithChildren.putAll(subFieldsWithChildren); + } + } + // 简单类型且没有注解的字段不需要处理 + } + + /** + * 判断是否为复杂类型(非基本类型、包装类型或String) + * + * @param clazz 要判断的类 + * @return 是否为复杂类型 + */ + private static boolean isComplexType(Class clazz) { + // 基本类型 + if (clazz.isPrimitive()) { + return false; + } + + // 常见的包装类型和String类型 + return !(clazz.equals(String.class) || + clazz.equals(Integer.class) || + clazz.equals(Long.class) || + clazz.equals(Boolean.class) || + clazz.equals(Double.class) || + clazz.equals(Float.class) || + clazz.equals(Character.class) || + clazz.equals(Byte.class) || + clazz.equals(Short.class) || + clazz.equals(java.util.Date.class) || + clazz.equals(java.time.LocalDate.class) || + clazz.equals(java.time.LocalDateTime.class)); + } + + @NotNull + public static List getHeaders(Map fieldsMap) { + return fieldsMap.entrySet().stream() + .sorted(Comparator.comparingInt(entry -> entry.getValue().getTableProperty().order())) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/utils/StringUtil.java b/src/main/java/cn/isliu/core/utils/StringUtil.java new file mode 100644 index 0000000..4669aa6 --- /dev/null +++ b/src/main/java/cn/isliu/core/utils/StringUtil.java @@ -0,0 +1,128 @@ +package cn.isliu.core.utils; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Map; +import java.util.List; +import java.util.Collection; +import java.util.Iterator; + +public class StringUtil { + + public static String mapToJson (Map map) { + StringBuilder jsonBuilder = new StringBuilder(); + jsonBuilder.append("{"); + Object[] keys = map.keySet().toArray(); + for (int i = 0; i < keys.length; i++) { + String key = (String) keys[i]; + Object value = map.get(key); + jsonBuilder.append("\"").append(key).append("\":"); + + if (value instanceof String) { + jsonBuilder.append("\"").append(value).append("\""); + } else { + jsonBuilder.append(value); + } + + if (i < keys.length - 1) { + jsonBuilder.append(","); + } + } + jsonBuilder.append("}"); + + return jsonBuilder.toString(); + } + + public static String getSHA256(String str) { + String uniqueId; + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hashBytes = digest.digest(str.getBytes(StandardCharsets.UTF_8)); + + // 转换为十六进制字符串 + StringBuilder hexString = new StringBuilder(); + for (byte b : hashBytes) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + uniqueId = hexString.toString(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("SHA-256 algorithm not available", e); + } + return uniqueId; + } + + /** + * 下划线转驼峰命名 + * @param underscoreName 下划线命名的字符串 + * @return 驼峰命名的字符串 + */ + public static String toCamelCase(String underscoreName) { + if (underscoreName == null || underscoreName.isEmpty()) { + return underscoreName; + } + + StringBuilder result = new StringBuilder(); + boolean capitalizeNext = false; + + for (char c : underscoreName.toCharArray()) { + if (c == '_') { + capitalizeNext = true; + } else if (capitalizeNext) { + result.append(Character.toUpperCase(c)); + capitalizeNext = false; + } else { + result.append(Character.toLowerCase(c)); + } + } + + return result.toString(); + } + + /** + * 将集合转换为逗号分隔的字符串 + * @param collection 集合对象 + * @return 逗号分隔的字符串 + */ + public static String joinWithComma(Collection collection) { + if (collection == null || collection.isEmpty()) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + Iterator iterator = collection.iterator(); + while (iterator.hasNext()) { + Object obj = iterator.next(); + sb.append(obj != null ? obj.toString() : ""); + if (iterator.hasNext()) { + sb.append(","); + } + } + return sb.toString(); + } + + /** + * 将数组转换为逗号分隔的字符串 + * @param array 数组对象 + * @return 逗号分隔的字符串 + */ + public static String joinWithComma(Object[] array) { + if (array == null || array.length == 0) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < array.length; i++) { + sb.append(array[i] != null ? array[i].toString() : ""); + if (i < array.length - 1) { + sb.append(","); + } + } + return sb.toString(); + } + +}