使用sql查询excel内容

[复制链接]
发表于 2025-11-4 19:50:31 | 显示全部楼层 |阅读模式
1. 简介


我们在前面的文章中提到了calcite支持csv和json文件的数据源适配, 实在就是将文件分析成表然后以文件夹为schema, 然后将天生的schema注册到RootSehema(RootSchema是全部数据源schema的parent,多个差别数据源schema可以挂在同一个RootSchema下)下, 终极使用calcite的特性举行sql的分析查询返回.

但实在我们的数据文件一样平常使用excel举行存储,流转, 但很惋惜, calcite本身没有excel的适配器, 但实在我们可以模仿calcite-file, 本身搞一个calcite-file-excel, 也可以认识calcite的工作原理.

2. 实现思绪


由于excel有sheet的概念, 以是可以将一个excel分析成schema, 每个sheet分析成table, 实现步调如下:


  • 实现SchemaFactory重写create方法: schema工厂 用于创建schema
  • 继续AbstractSchema: schema形貌类 用于分析excel, 创建table(分析sheet)
  • 继续AbstractTable, ScannableTable: table形貌类 提供字段信息和数据内容等(分析sheet data)

3. Excel样例


excel有两个sheet页, 分别是user_info 和 role_info如下:







ok, 万事具备.

4. Maven


  1. <dependency>
  2.     <groupId>org.apache.poi</groupId>
  3.     <artifactId>poi-ooxml</artifactId>
  4.     <version>5.2.3</version>
  5. </dependency>
  6. <dependency>
  7.     <groupId>org.apache.poi</groupId>
  8.     <artifactId>poi</artifactId>
  9.     <version>5.2.3</version>
  10. </dependency>
  11. <dependency>
  12.     <groupId>org.apache.calcite</groupId>
  13.     <artifactId>calcite-core</artifactId>
  14.     <version>1.37.0</version>
  15. </dependency>
复制代码

5. 焦点代码


5.1 SchemaFactory


  1. package com.ldx.calcite.excel;
  2. import com.google.common.collect.Lists;
  3. import org.apache.calcite.schema.Schema;
  4. import org.apache.calcite.schema.SchemaFactory;
  5. import org.apache.calcite.schema.SchemaPlus;
  6. import org.apache.commons.lang3.ObjectUtils;
  7. import org.apache.commons.lang3.StringUtils;
  8. import java.io.File;
  9. import java.util.List;
  10. import java.util.Map;
  11. /**
  12. * schema factory
  13. */
  14. public class ExcelSchemaFactory implements SchemaFactory {
  15.     public final static ExcelSchemaFactory INSTANCE = new ExcelSchemaFactory();
  16.     private ExcelSchemaFactory(){}
  17.     @Override
  18.     public Schema create(SchemaPlus parentSchema, String name, Map<String, Object> operand) {
  19.         final Object filePath = operand.get("filePath");
  20.         if (ObjectUtils.isEmpty(filePath)) {
  21.             throw new NullPointerException("can not find excel file");
  22.         }
  23.         return this.create(filePath.toString());
  24.     }
  25.     public Schema create(String excelFilePath) {
  26.         if (StringUtils.isBlank(excelFilePath)) {
  27.             throw new NullPointerException("can not find excel file");
  28.         }
  29.         return this.create(new File(excelFilePath));
  30.     }
  31.     public Schema create(File excelFile) {
  32.         if (ObjectUtils.isEmpty(excelFile) || !excelFile.exists()) {
  33.             throw new NullPointerException("can not find excel file");
  34.         }
  35.         if (!excelFile.isFile() || !isExcelFile(excelFile)) {
  36.             throw new RuntimeException("can not find excel file: " + excelFile.getAbsolutePath());
  37.         }
  38.         return new ExcelSchema(excelFile);
  39.     }
  40.     protected List<String> supportedFileSuffix() {
  41.         return Lists.newArrayList("xls", "xlsx");
  42.     }
  43.     private boolean isExcelFile(File excelFile) {
  44.         if (ObjectUtils.isEmpty(excelFile)) {
  45.             return false;
  46.         }
  47.         final String name = excelFile.getName();
  48.         return StringUtils.endsWithAny(name, this.supportedFileSuffix().toArray(new String[0]));
  49.     }
  50. }
复制代码

schema中有多个重载的create方法用于方便的创建schema, 终极将excel file 交给ExcelSchema创建一个schema对象

5.2 Schema


  1. package com.ldx.calcite.excel;
  2. import org.apache.calcite.schema.Table;
  3. import org.apache.calcite.schema.impl.AbstractSchema;
  4. import org.apache.commons.lang3.ObjectUtils;
  5. import org.apache.poi.ss.usermodel.Sheet;
  6. import org.apache.poi.ss.usermodel.Workbook;
  7. import org.apache.poi.ss.usermodel.WorkbookFactory;
  8. import org.testng.collections.Maps;
  9. import java.io.File;
  10. import java.util.Iterator;
  11. import java.util.Map;
  12. /**
  13. * schema
  14. */
  15. public class ExcelSchema extends AbstractSchema {
  16.     private final File excelFile;
  17.     private Map<String, Table> tableMap;
  18.     public ExcelSchema(File excelFile) {
  19.         this.excelFile = excelFile;
  20.     }
  21.     @Override
  22.     protected Map<String, Table> getTableMap() {
  23.         if (ObjectUtils.isEmpty(tableMap)) {
  24.             tableMap = createTableMap();
  25.         }
  26.         return tableMap;
  27.     }
  28.     private Map<String, Table> createTableMap() {
  29.         final Map<String, Table> result = Maps.newHashMap();
  30.         try (Workbook workbook = WorkbookFactory.create(excelFile)) {
  31.             final Iterator<Sheet> sheetIterator = workbook.sheetIterator();
  32.             while (sheetIterator.hasNext()) {
  33.                 final Sheet sheet = sheetIterator.next();
  34.                 final ExcelScannableTable excelScannableTable = new ExcelScannableTable(sheet, null);
  35.                 result.put(sheet.getSheetName(), excelScannableTable);
  36.             }
  37.         }
  38.         catch (Exception ignored) {}
  39.         return result;
  40.     }
  41. }
复制代码

schema类读取Excel file, 并循环读取sheet, 将每个sheet分析成ExcelScannableTable存储

5.3 Table


  1. package com.ldx.calcite.excel;
  2. import com.google.common.collect.Lists;
  3. import com.ldx.calcite.excel.enums.JavaFileTypeEnum;
  4. import org.apache.calcite.DataContext;
  5. import org.apache.calcite.adapter.java.JavaTypeFactory;
  6. import org.apache.calcite.linq4j.Enumerable;
  7. import org.apache.calcite.linq4j.Linq4j;
  8. import org.apache.calcite.rel.type.RelDataType;
  9. import org.apache.calcite.rel.type.RelDataTypeFactory;
  10. import org.apache.calcite.rel.type.RelProtoDataType;
  11. import org.apache.calcite.schema.ScannableTable;
  12. import org.apache.calcite.schema.impl.AbstractTable;
  13. import org.apache.calcite.sql.type.SqlTypeName;
  14. import org.apache.calcite.util.Pair;
  15. import org.apache.commons.lang3.ObjectUtils;
  16. import org.apache.poi.ss.usermodel.Cell;
  17. import org.apache.poi.ss.usermodel.Row;
  18. import org.apache.poi.ss.usermodel.Sheet;
  19. import org.checkerframework.checker.nullness.qual.Nullable;
  20. import java.util.List;
  21. /**
  22. * table
  23. */
  24. public class ExcelScannableTable extends AbstractTable implements ScannableTable {
  25.     private final RelProtoDataType protoRowType;
  26.     private final Sheet sheet;
  27.     private RelDataType rowType;
  28.     private List<JavaFileTypeEnum> fieldTypes;
  29.     private List<Object[]> rowDataList;
  30.     public ExcelScannableTable(Sheet sheet, RelProtoDataType protoRowType) {
  31.         this.protoRowType = protoRowType;
  32.         this.sheet = sheet;
  33.     }
  34.     @Override
  35.     public Enumerable<@Nullable Object[]> scan(DataContext root) {
  36.         JavaTypeFactory typeFactory = root.getTypeFactory();
  37.         final List<JavaFileTypeEnum> fieldTypes = this.getFieldTypes(typeFactory);
  38.         if (rowDataList == null) {
  39.             rowDataList = readExcelData(sheet, fieldTypes);
  40.         }
  41.         return Linq4j.asEnumerable(rowDataList);
  42.     }
  43.     @Override
  44.     public RelDataType getRowType(RelDataTypeFactory typeFactory) {
  45.         if (ObjectUtils.isNotEmpty(protoRowType)) {
  46.             return protoRowType.apply(typeFactory);
  47.         }
  48.         if (ObjectUtils.isEmpty(rowType)) {
  49.             rowType = deduceRowType((JavaTypeFactory) typeFactory, sheet, null);
  50.         }
  51.         return rowType;
  52.     }
  53.     public List<JavaFileTypeEnum> getFieldTypes(RelDataTypeFactory typeFactory) {
  54.         if (fieldTypes == null) {
  55.             fieldTypes = Lists.newArrayList();
  56.             deduceRowType((JavaTypeFactory) typeFactory, sheet, fieldTypes);
  57.         }
  58.         return fieldTypes;
  59.     }
  60.     private List<Object[]> readExcelData(Sheet sheet, List<JavaFileTypeEnum> fieldTypes) {
  61.         List<Object[]> rowDataList = Lists.newArrayList();
  62.         for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
  63.             Row row = sheet.getRow(rowIndex);
  64.             Object[] rowData = new Object[fieldTypes.size()];
  65.             for (int i = 0; i < row.getLastCellNum(); i++) {
  66.                 final JavaFileTypeEnum javaFileTypeEnum = fieldTypes.get(i);
  67.                 Cell cell = row.getCell(i, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK);
  68.                 final Object cellValue = javaFileTypeEnum.getCellValue(cell);
  69.                 rowData[i] = cellValue;
  70.             }
  71.             rowDataList.add(rowData);
  72.         }
  73.         return rowDataList;
  74.     }
  75.     public static RelDataType deduceRowType(JavaTypeFactory typeFactory, Sheet sheet, List<JavaFileTypeEnum> fieldTypes) {
  76.         final List<String> names = Lists.newArrayList();
  77.         final List<RelDataType> types = Lists.newArrayList();
  78.         if (sheet != null) {
  79.             Row headerRow = sheet.getRow(0);
  80.             if (headerRow != null) {
  81.                 for (int i = 0; i < headerRow.getLastCellNum(); i++) {
  82.                     Cell cell = headerRow.getCell(i, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK);
  83.                     String[] columnInfo = cell
  84.                             .getStringCellValue()
  85.                             .split(":");
  86.                     String columnName = columnInfo[0].trim();
  87.                     String columnType = null;
  88.                     if (columnInfo.length == 2) {
  89.                         columnType = columnInfo[1].trim();
  90.                     }
  91.                     final JavaFileTypeEnum javaFileType = JavaFileTypeEnum
  92.                             .of(columnType)
  93.                             .orElse(JavaFileTypeEnum.UNKNOWN);
  94.                     final RelDataType sqlType = typeFactory.createSqlType(javaFileType.getSqlTypeName());
  95.                     names.add(columnName);
  96.                     types.add(sqlType);
  97.                     if (fieldTypes != null) {
  98.                         fieldTypes.add(javaFileType);
  99.                     }
  100.                 }
  101.             }
  102.         }
  103.         if (names.isEmpty()) {
  104.             names.add("line");
  105.             types.add(typeFactory.createSqlType(SqlTypeName.VARCHAR));
  106.         }
  107.         return typeFactory.createStructType(Pair.zip(names, types));
  108.     }
  109. }
复制代码

table类中此中有两个比力关键的方法

​ scan: 扫描表内容, 我们这里将sheet页面的数据内容分析存储末了交给calcite

​ getRowType: 获取字段信息, 我们这里默认使用第一条纪录作为表头(row[0]) 并分析为字段信息, 字段规则跟csv一样 name:string, 冒号前面的是字段key, 冒号背面的是字段范例, 如果未指定字段范例, 则分析为UNKNOWN, 后续JavaFileTypeEnum会举行范例推断, 终极在效果处置惩罚时calcite也会举行推断

deduceRowType: 推断字段范例, 方法中使用JavaFileTypeEnum罗列类对java type & sql type & 字段值转化处置惩罚方法 举行管理

5.4 ColumnTypeEnum


  1. package com.ldx.calcite.excel.enums;
  2. import lombok.Getter;
  3. import lombok.extern.slf4j.Slf4j;
  4. import org.apache.calcite.avatica.util.DateTimeUtils;
  5. import org.apache.calcite.sql.type.SqlTypeName;
  6. import org.apache.commons.lang3.ObjectUtils;
  7. import org.apache.commons.lang3.StringUtils;
  8. import org.apache.commons.lang3.time.FastDateFormat;
  9. import org.apache.poi.ss.usermodel.Cell;
  10. import org.apache.poi.ss.usermodel.DateUtil;
  11. import org.apache.poi.ss.util.CellUtil;
  12. import java.text.ParseException;
  13. import java.text.SimpleDateFormat;
  14. import java.util.Arrays;
  15. import java.util.Date;
  16. import java.util.Optional;
  17. import java.util.TimeZone;
  18. import java.util.function.Function;
  19. /**
  20. * type converter
  21. */
  22. @Slf4j
  23. @Getter
  24. public enum JavaFileTypeEnum {
  25.     STRING("string", SqlTypeName.VARCHAR, Cell::getStringCellValue),
  26.     BOOLEAN("boolean", SqlTypeName.BOOLEAN, Cell::getBooleanCellValue),
  27.     BYTE("byte", SqlTypeName.TINYINT, Cell::getStringCellValue),
  28.     CHAR("char", SqlTypeName.CHAR, Cell::getStringCellValue),
  29.     SHORT("short", SqlTypeName.SMALLINT, Cell::getNumericCellValue),
  30.     INT("int", SqlTypeName.INTEGER, cell -> (Double.valueOf(cell.getNumericCellValue()).intValue())),
  31.     LONG("long", SqlTypeName.BIGINT, cell -> (Double.valueOf(cell.getNumericCellValue()).longValue())),
  32.     FLOAT("float", SqlTypeName.REAL, Cell::getNumericCellValue),
  33.     DOUBLE("double", SqlTypeName.DOUBLE, Cell::getNumericCellValue),
  34.     DATE("date", SqlTypeName.DATE, getValueWithDate()),
  35.     TIMESTAMP("timestamp", SqlTypeName.TIMESTAMP, getValueWithTimestamp()),
  36.     TIME("time", SqlTypeName.TIME, getValueWithTime()),
  37.     UNKNOWN("unknown", SqlTypeName.UNKNOWN, getValueWithUnknown()),;
  38.     // cell type
  39.     private final String typeName;
  40.                 // sql type
  41.     private final SqlTypeName sqlTypeName;
  42.     // value convert func
  43.     private final Function<Cell, Object> cellValueFunc;
  44.     private static final FastDateFormat TIME_FORMAT_DATE;
  45.     private static final FastDateFormat TIME_FORMAT_TIME;
  46.     private static final FastDateFormat TIME_FORMAT_TIMESTAMP;
  47.     static {
  48.         final TimeZone gmt = TimeZone.getTimeZone("GMT");
  49.         TIME_FORMAT_DATE = FastDateFormat.getInstance("yyyy-MM-dd", gmt);
  50.         TIME_FORMAT_TIME = FastDateFormat.getInstance("HH:mm:ss", gmt);
  51.         TIME_FORMAT_TIMESTAMP = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss", gmt);
  52.     }
  53.     JavaFileTypeEnum(String typeName, SqlTypeName sqlTypeName, Function<Cell, Object> cellValueFunc) {
  54.         this.typeName = typeName;
  55.         this.sqlTypeName = sqlTypeName;
  56.         this.cellValueFunc = cellValueFunc;
  57.     }
  58.     public static Optional<JavaFileTypeEnum> of(String typeName) {
  59.         return Arrays
  60.                 .stream(values())
  61.                 .filter(type -> StringUtils.equalsIgnoreCase(typeName, type.getTypeName()))
  62.                 .findFirst();
  63.     }
  64.     public static SqlTypeName findSqlTypeName(String typeName) {
  65.         final Optional<JavaFileTypeEnum> javaFileTypeOptional = of(typeName);
  66.         if (javaFileTypeOptional.isPresent()) {
  67.             return javaFileTypeOptional
  68.                     .get()
  69.                     .getSqlTypeName();
  70.         }
  71.         return SqlTypeName.UNKNOWN;
  72.     }
  73.     public Object getCellValue(Cell cell) {
  74.         return cellValueFunc.apply(cell);
  75.     }
  76.     public static Function<Cell, Object> getValueWithUnknown() {
  77.         return cell -> {
  78.             if (ObjectUtils.isEmpty(cell)) {
  79.                 return null;
  80.             }
  81.             switch (cell.getCellType()) {
  82.                 case STRING:
  83.                     return cell.getStringCellValue();
  84.                 case NUMERIC:
  85.                     if (DateUtil.isCellDateFormatted(cell)) {
  86.                         // 如果是日期类型,返回日期对象
  87.                         return cell.getDateCellValue();
  88.                     }
  89.                     else {
  90.                         // 否则返回数值
  91.                         return cell.getNumericCellValue();
  92.                     }
  93.                 case BOOLEAN:
  94.                     return cell.getBooleanCellValue();
  95.                 case FORMULA:
  96.                     // 对于公式单元格,先计算公式结果,再获取其值
  97.                     try {
  98.                         return cell.getNumericCellValue();
  99.                     }
  100.                     catch (Exception e) {
  101.                         try {
  102.                             return cell.getStringCellValue();
  103.                         }
  104.                         catch (Exception ex) {
  105.                             log.error("parse unknown data error, cellRowIndex:{}, cellColumnIndex:{}", cell.getRowIndex(), cell.getColumnIndex(), e);
  106.                             return null;
  107.                         }
  108.                     }
  109.                 case BLANK:
  110.                     return "";
  111.                 default:
  112.                     return null;
  113.             }
  114.         };
  115.     }
  116.     public static Function<Cell, Object> getValueWithDate() {
  117.         return cell -> {
  118.             Date date = cell.getDateCellValue();
  119.             if(ObjectUtils.isEmpty(date)) {
  120.                 return null;
  121.             }
  122.             try {
  123.                 final String formated = new SimpleDateFormat("yyyy-MM-dd").format(date);
  124.                 Date newDate = TIME_FORMAT_DATE.parse(formated);
  125.                 return (int) (newDate.getTime() / DateTimeUtils.MILLIS_PER_DAY);
  126.             }
  127.             catch (ParseException e) {
  128.                 log.error("parse date error, date:{}", date, e);
  129.             }
  130.             return null;
  131.         };
  132.     }
  133.     public static Function<Cell, Object> getValueWithTimestamp() {
  134.         return cell -> {
  135.             Date date = cell.getDateCellValue();
  136.             if(ObjectUtils.isEmpty(date)) {
  137.                 return null;
  138.             }
  139.             try {
  140.                 final String formated = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
  141.                 Date newDate = TIME_FORMAT_TIMESTAMP.parse(formated);
  142.                 return (int) newDate.getTime();
  143.             }
  144.             catch (ParseException e) {
  145.                 log.error("parse timestamp error, date:{}", date, e);
  146.             }
  147.             return null;
  148.         };
  149.     }
  150.     public static Function<Cell, Object> getValueWithTime() {
  151.         return cell -> {
  152.             Date date = cell.getDateCellValue();
  153.             if(ObjectUtils.isEmpty(date)) {
  154.                 return null;
  155.             }
  156.             try {
  157.                 final String formated = new SimpleDateFormat("HH:mm:ss").format(date);
  158.                 Date newDate = TIME_FORMAT_TIME.parse(formated);
  159.                 return newDate.getTime();
  160.             }
  161.             catch (ParseException e) {
  162.                 log.error("parse time error, date:{}", date, e);
  163.             }
  164.             return null;
  165.         };
  166.     }
  167. }
复制代码

该罗列类紧张管理了java typesql type & cell value convert func, 方便同一管理范例映射及单位格内容提取时的转换方法(这里借用了java8 function函数特性)

   注: 这里的日期转换只能如许写, 纵然用GMT的时区(抄的calcite-file), 要否则输出的日期时间不绝偶尔差...
  
6. 测试查询


  1. package com.ldx.calcite;
  2. import com.ldx.calcite.excel.ExcelSchemaFactory;
  3. import lombok.SneakyThrows;
  4. import lombok.extern.slf4j.Slf4j;
  5. import org.apache.calcite.config.CalciteConnectionProperty;
  6. import org.apache.calcite.jdbc.CalciteConnection;
  7. import org.apache.calcite.schema.Schema;
  8. import org.apache.calcite.schema.SchemaPlus;
  9. import org.apache.calcite.util.Sources;
  10. import org.junit.jupiter.api.AfterAll;
  11. import org.junit.jupiter.api.BeforeAll;
  12. import org.junit.jupiter.api.Test;
  13. import org.testng.collections.Maps;
  14. import java.net.URL;
  15. import java.sql.Connection;
  16. import java.sql.DriverManager;
  17. import java.sql.ResultSet;
  18. import java.sql.ResultSetMetaData;
  19. import java.sql.SQLException;
  20. import java.sql.Statement;
  21. import java.util.Map;
  22. import java.util.Properties;
  23. @Slf4j
  24. public class CalciteExcelTest {
  25.     private static Connection connection;
  26.     private static SchemaPlus rootSchema;
  27.     private static CalciteConnection calciteConnection;
  28.     @BeforeAll
  29.     @SneakyThrows
  30.     public static void beforeAll() {
  31.         Properties info = new Properties();
  32.         // 不区分sql大小写
  33.         info.setProperty(CalciteConnectionProperty.CASE_SENSITIVE.camelName(), "false");
  34.         // 创建Calcite连接
  35.         connection = DriverManager.getConnection("jdbc:calcite:", info);
  36.         calciteConnection = connection.unwrap(CalciteConnection.class);
  37.         // 构建RootSchema,在Calcite中,RootSchema是所有数据源schema的parent,多个不同数据源schema可以挂在同一个RootSchema下
  38.         rootSchema = calciteConnection.getRootSchema();
  39.     }
  40.     @Test
  41.     @SneakyThrows
  42.     public void test_execute_query() {
  43.         final Schema schema = ExcelSchemaFactory.INSTANCE.create(resourcePath("file/test.xlsx"));
  44.         rootSchema.add("test", schema);
  45.         // 设置默认的schema
  46.         calciteConnection.setSchema("test");
  47.         final Statement statement = calciteConnection.createStatement();
  48.         ResultSet resultSet = statement.executeQuery("SELECT * FROM user_info");
  49.         printResultSet(resultSet);
  50.         System.out.println("=========");
  51.         ResultSet resultSet2 = statement.executeQuery("SELECT * FROM test.user_info where id > 110 and birthday > '2003-01-01'");
  52.         printResultSet(resultSet2);
  53.         System.out.println("=========");
  54.         ResultSet resultSet3 = statement.executeQuery("SELECT * FROM test.user_info ui inner join  test.role_info ri on ui.role_id = ri.id");
  55.         printResultSet(resultSet3);
  56.     }
  57.     @AfterAll
  58.     @SneakyThrows
  59.     public static void closeResource() {
  60.         connection.close();
  61.     }
  62.     private static String resourcePath(String path) {
  63.         final URL url = CalciteExcelTest.class.getResource("/" + path);
  64.         return Sources.of(url).file().getAbsolutePath();
  65.     }
  66.     public static void printResultSet(ResultSet resultSet) throws SQLException {
  67.         // 获取 ResultSet 元数据
  68.         ResultSetMetaData metaData = resultSet.getMetaData();
  69.         // 获取列数
  70.         int columnCount = metaData.getColumnCount();
  71.         log.info("Number of columns: {}",columnCount);
  72.         // 遍历 ResultSet 并打印结果
  73.         while (resultSet.next()) {
  74.             final Map<String, String> item = Maps.newHashMap();
  75.             // 遍历每一列并打印
  76.             for (int i = 1; i <= columnCount; i++) {
  77.                 String columnName = metaData.getColumnName(i);
  78.                 String columnValue = resultSet.getString(i);
  79.                 item.put(columnName, columnValue);
  80.             }
  81.             log.info(item.toString());
  82.         }
  83.     }
  84. }
复制代码

测试效果如下:




   文章转载自:张铁牛
  原文链接:4. 使用sql查询excel内容 - 张铁牛 - 博客园
  体验地点:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程计划器_表单引擎_工作流引擎_软件架构

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
继续阅读请点击广告

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
回复

使用道具 举报

×
登录参与点评抽奖,加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表