【feat】 初步实现数据库同步到本地

This commit is contained in:
Kris 2026-01-06 18:54:13 +08:00
parent a78da492c7
commit cb09602386
14 changed files with 1161 additions and 746 deletions

View File

@ -330,4 +330,11 @@ public final class SalesforceConstants {
* 格式示例: 2024-01-01
*/
public static final String SF_DATE_ONLY_FORMAT = "yyyy-MM-dd";
public static final String SF_DATE_FORMAT_BULK = "yyyy-MM-dd'T'HH:mm:ss.SSSX";
/**
* sf 时间格式
*/
public static final String SF_TIME_FORMAT = "HH:mm:ss.SSS'Z'";
}

View File

@ -118,6 +118,11 @@ public class DataiIntegrationObject extends BaseEntity
@Excel(name = "批次字段")
private String batchField;
/** 是否分区 */
@Schema(title = "是否分区")
@Excel(name = "是否分区")
private Boolean isPartitioned;
/** 本地记录数 */
@Schema(title = "本地记录数")
@Excel(name = "本地记录数")
@ -128,10 +133,10 @@ public class DataiIntegrationObject extends BaseEntity
@Excel(name = "最近同步时间")
private LocalDateTime lastSyncDate;
/** 全量同步时间 */
@Schema(title = "全量同步时间")
@Excel(name = "全量同步时间")
private LocalDateTime lastFullSyncDate;
/** 最后批次时间 */
@Schema(title = "最后批次时间")
@Excel(name = "最后批次时间")
private LocalDateTime lastBatchDate;
/** 状态 */
@Schema(title = "状态")
@ -362,6 +367,17 @@ public class DataiIntegrationObject extends BaseEntity
}
public void setIsPartitioned(Boolean isPartitioned)
{
this.isPartitioned = isPartitioned;
}
public Boolean getIsPartitioned()
{
return isPartitioned;
}
public void setTotalRows(Integer totalRows)
{
this.totalRows = totalRows;
@ -384,14 +400,14 @@ public class DataiIntegrationObject extends BaseEntity
}
public void setLastFullSyncDate(LocalDateTime lastFullSyncDate)
public void setLastBatchDate(LocalDateTime lastBatchDate)
{
this.lastFullSyncDate = lastFullSyncDate;
this.lastBatchDate = lastBatchDate;
}
public LocalDateTime getLastFullSyncDate()
public LocalDateTime getLastBatchDate()
{
return lastFullSyncDate;
return lastBatchDate;
}
@ -441,9 +457,10 @@ public class DataiIntegrationObject extends BaseEntity
.append("isIncremental", getIsIncremental())
.append("objectIndex", getObjectIndex())
.append("batchField", getBatchField())
.append("isPartitioned", getIsPartitioned())
.append("totalRows", getTotalRows())
.append("lastSyncDate", getLastSyncDate())
.append("lastFullSyncDate", getLastFullSyncDate())
.append("lastBatchDate", getLastBatchDate())
.append("syncStatus", getSyncStatus())
.append("errorMessage", getErrorMessage())
.append("remark", getRemark())

View File

@ -84,14 +84,17 @@ public class DataiIntegrationObjectDto implements Serializable
/** 批次字段 */
private String batchField;
/** 是否分区 */
private Boolean isPartitioned;
/** 本地记录数 */
private Integer totalRows;
/** 最近同步时间 */
private LocalDateTime lastSyncDate;
/** 全量同步时间 */
private LocalDateTime lastFullSyncDate;
/** 最后批次时间 */
private LocalDateTime lastBatchDate;
/** 状态 */
private Boolean syncStatus;

View File

@ -9,7 +9,7 @@ import java.util.Date;
import java.util.Set;
/**
* @author Radian
* @author Kris
* @description
* @date 2022/11/30
*/
@ -33,16 +33,45 @@ public class DataiSyncParam {
*/
private Date endDate;
/**
* 同步类型 1存量 2增量
* 开始创建时间
*/
private Integer type;
private Date beginCreateDate;
/**
* fastjson 过滤器 只打印当前类所有属性
* @return SimplePropertyPreFilter
* 结束创建时间
*/
public static SimplePropertyPreFilter getFilter() {
return new SimplePropertyPreFilter(DataiSyncParam.class, Arrays.stream(DataiSyncParam.class.getDeclaredFields()).map(Field::getName).toArray(String[]::new));
}
private Date endCreateDate;
/**
* 开始修改时间
*/
private Date beginModifyDate;
/**
* 结束修改时间
*/
private Date endModifyDate;
/**
* 最大Id
*/
private String maxId;
/**
* id字段
*/
private String idField = "Id";
/**
* 时间字段
*/
private String batchField = "CreatedDate";
/**
* 查询限制数量
*/
private Integer limit;
/**
* 查询字段 多个英文逗号分割
*/
private String select;
/**
* 是否查询删除记录
*/
private Boolean isDeleted;
}

View File

@ -79,14 +79,17 @@ public class DataiIntegrationObjectVo implements Serializable {
/** 批次字段 */
private String batchField;
/** 是否分区 */
private Boolean isPartitioned;
/** 本地记录数 */
private Integer totalRows;
/** 最近同步时间 */
private LocalDateTime lastSyncDate;
/** 全量同步时间 */
private LocalDateTime lastFullSyncDate;
/** 最后批次时间 */
private LocalDateTime lastBatchDate;
/** 状态 */
private Boolean syncStatus;

View File

@ -1,17 +1,18 @@
package com.datai.integration.service.impl;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.*;
import java.util.function.Supplier;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.datai.common.utils.DateUtils;
import com.datai.common.utils.SecurityUtils;
import com.datai.integration.model.param.DataiSyncParam;
import com.datai.salesforce.common.utils.SoqlBuilder;
import com.datai.integration.util.ConvertUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -356,14 +357,14 @@ public class DataiIntegrationBatchServiceImpl implements IDataiIntegrationBatchS
}
PartnerConnection connection = retryOperation(() -> soapConnectionFactory.getConnection(), 3, 1000);
log.info("成功获取Salesforce SOAP连接");
List<String> fieldList = getSalesforceObjectFields(connection, objectApi);
log.info("获取到对象 {} 的字段列表: {}", objectApi, fieldList);
SalesforceParam param = new SalesforceParam();
DataiSyncParam param = new DataiSyncParam();
param.setApi(objectApi);
param.setSelect(String.join(",", fieldList));
param.setBatchField(batch.getBatchField());
if (integrationFieldService.isDeletedFieldExists(objectApi)) {
param.setIsDeleted(true);
@ -391,7 +392,7 @@ public class DataiIntegrationBatchServiceImpl implements IDataiIntegrationBatchS
batchHistoryService.insertDataiIntegrationBatchHistory(batchHistory);
log.info("批次 {} 历史记录已保存,同步数据量: {}, 耗时: {}ms", batchId, totalCount, duration);
insertSyncLog(objectApi, batch.getLabel(), batch.getSyncType(), true, null, duration);
insertSyncLog(objectApi, batch.getLabel(), batch.getSyncType(), Integer.parseInt(batchId), true, null, duration);
return true;
} catch (Exception e) {
@ -416,16 +417,17 @@ public class DataiIntegrationBatchServiceImpl implements IDataiIntegrationBatchS
batchHistoryService.insertDataiIntegrationBatchHistory(batchHistory);
log.info("批次 {} 失败历史记录已保存,耗时: {}ms", batchId, duration);
insertSyncLog(objectApi, batch != null ? batch.getLabel() : "", batch != null ? batch.getSyncType() : "", false, e.getMessage(), duration);
insertSyncLog(objectApi, batch != null ? batch.getLabel() : "", batch != null ? batch.getSyncType() : "", Integer.parseInt(batchId), false, e.getMessage(), duration);
return false;
}
}
private void insertSyncLog(String objectApi, String label, String syncType, boolean success, String errorMessage, long duration) {
private void insertSyncLog(String objectApi, String label, String syncType, Integer batchId, boolean success, String errorMessage, long duration) {
try {
com.datai.integration.model.domain.DataiIntegrationSyncLog syncLog = new com.datai.integration.model.domain.DataiIntegrationSyncLog();
syncLog.setObjectApi(objectApi);
syncLog.setBatchId(batchId);
syncLog.setOperationType("FULL".equals(syncType) ? "全量同步" : "增量同步");
syncLog.setOperationStatus(success ? "成功" : "失败");
syncLog.setErrorMessage(errorMessage);
@ -519,17 +521,38 @@ public class DataiIntegrationBatchServiceImpl implements IDataiIntegrationBatchS
* @param fieldList 字段列表
* @return 处理的数据条数
*/
private int executeQueryAndProcessData(PartnerConnection connection, SalesforceParam param, List<String> fieldList) {
private int executeQueryAndProcessData(PartnerConnection connection, DataiSyncParam param, List<String> fieldList) {
int totalCount = 0;
try {
String query = buildDynamicQuery(param);
log.info("执行查询SQL: {}", query);
JSONArray objects = null;
String maxId = null;
Date lastDate = null;
QueryResult result = connection.queryAll(query);
try {
DescribeSObjectResult describeSObject = connection.describeSObject(param.getApi());
Field[] dsrFields = describeSObject.getFields();
while (true) {
// 获取创建时间
param.setBeginDate(lastDate);
// 判断是否存在要排除的id
param.setMaxId(maxId);
String query = buildDynamicQuery(param);
log.info("执行查询SQL: {}", query);
QueryResult result = connection.queryAll(query);
if (result.getRecords() != null && result.getRecords().length > 0) {
totalCount += processQueryResult(param.getApi(), result, fieldList);
objects = ConvertUtil.toJsonArray(result.getRecords(), dsrFields);
Date maxDate = objects.getJSONObject(objects.size() - 1).getDate(param.getBatchField());
// 在当前批次中找出所有具有相同时间戳的记录并获取其中ID最大的那条记录的ID
// 这个ID将作为下一批次查询的起始位置避免重复处理相同时间戳的数据
Optional<String> maxIdOptional = objects.stream()
.map(t -> (JSONObject) t)
.filter(t -> maxDate.equals(t.getDate(param.getBatchField())))
.map(t -> t.getString("Id"))
.max(String::compareTo);
maxId = maxIdOptional.orElse(null);
lastDate = maxDate;
totalCount += processQueryResult(param.getApi(), result.getRecords(), objects);
log.info("已处理 {} 条记录", totalCount);
}
@ -556,35 +579,65 @@ public class DataiIntegrationBatchServiceImpl implements IDataiIntegrationBatchS
* @param param 查询参数
* @return SOQL查询语句
*/
private String buildDynamicQuery(SalesforceParam param) {
StringBuilder queryBuilder = new StringBuilder();
queryBuilder.append("SELECT ").append(param.getSelect())
.append(" FROM ").append(param.getApi());
private String buildDynamicQuery(DataiSyncParam param) {
SoqlBuilder builder = new SoqlBuilder();
if (param.getIsDeleted() != null && param.getIsDeleted()) {
queryBuilder.append(" WHERE IsDeleted = true");
String[] selectFields = param.getSelect().split(",");
builder.select(selectFields)
.from(param.getApi());
if (param.getIsDeleted() != null && !param.getIsDeleted()) {
builder.where("IsDeleted = false");
}
return queryBuilder.toString();
if (param.getBeginDate() != null && param.getEndDate() != null) {
builder.whereGe(param.getBatchField(), param.getBeginDate())
.whereLe(param.getBatchField(), param.getEndDate());
} else if (param.getBeginDate() != null) {
builder.whereGe(param.getBatchField(), param.getBeginDate());
} else if (param.getEndDate() != null) {
builder.whereLe(param.getBatchField(), param.getEndDate());
}
if (param.getBeginCreateDate() != null && param.getEndCreateDate() != null) {
builder.whereGe(param.getBatchField(), param.getBeginCreateDate())
.whereLe(param.getBatchField(), param.getEndCreateDate());
} else if (param.getBeginCreateDate() != null) {
builder.whereGe(param.getBatchField(), param.getBeginCreateDate());
} else if (param.getEndCreateDate() != null) {
builder.whereLe(param.getBatchField(), param.getEndCreateDate());
}
if (param.getBeginModifyDate() != null && param.getEndModifyDate() != null) {
builder.whereGe(param.getBatchField(), param.getBeginModifyDate())
.whereLe(param.getBatchField(), param.getEndModifyDate());
} else if (param.getBeginModifyDate() != null) {
builder.whereGe(param.getBatchField(), param.getBeginModifyDate());
} else if (param.getEndModifyDate() != null) {
builder.whereLe(param.getBatchField(), param.getEndModifyDate());
}
if (StringUtils.isNotEmpty(param.getMaxId())) {
builder.whereGt(param.getIdField(), param.getMaxId());
}
if (param.getLimit() != null && param.getLimit() > 0) {
builder.limit(param.getLimit());
}
return builder.build();
}
/**
* 处理查询结果
*
* @param api API名称
* @param result 查询结果
* @param fieldList 字段列表
* @return 处理的数据条数
*/
private int processQueryResult(String api, QueryResult result, List<String> fieldList) {
private int processQueryResult(String api, SObject[] records, JSONArray objects) {
int count = 0;
if (result == null || result.getRecords() == null) {
log.info("处理API {} 的查询结果,共 0 条记录", api);
return count;
}
SObject[] records = result.getRecords();
log.info("处理API {} 的查询结果,共 {} 条记录", api, records.length);
boolean isPartitioned = checkIfTablePartitioned(api);
@ -593,135 +646,95 @@ public class DataiIntegrationBatchServiceImpl implements IDataiIntegrationBatchS
batchField = integrationFieldService.getDateField(api);
}
Map<String, List<Map<String, Object>>> partitionedData = new HashMap<>();
List<Map<String, Object>> normalData = new ArrayList<>();
for (SObject record : records) {
if (record == null) {
continue;
}
try {
Map<String, Object> recordMap = convertSObjectToMap(record, fieldList);
if (isPartitioned) {
String partitionName = "p_default";
if (StringUtils.isNotEmpty(batchField) && recordMap.containsKey(batchField.toLowerCase())) {
Object dateValue = recordMap.get(batchField.toLowerCase());
if (dateValue instanceof Date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime((Date) dateValue);
int year = calendar.get(Calendar.YEAR);
partitionName = "p" + year;
}
}
partitionedData.computeIfAbsent(partitionName, k -> new ArrayList<>()).add(recordMap);
} else {
normalData.add(recordMap);
}
count++;
} catch (Exception e) {
String recordId = record.getId() != null ? record.getId() : "未知";
log.error("处理记录时发生异常记录ID: {}", recordId, e);
}
if (objects == null || objects.isEmpty()) {
log.info("API {} 没有需要处理的数据", api);
return 0;
}
try {
if (isPartitioned) {
for (Map.Entry<String, List<Map<String, Object>>> entry : partitionedData.entrySet()) {
String partitionName = entry.getKey();
List<Map<String, Object>> dataList = entry.getValue();
if (!dataList.isEmpty()) {
if (!dataList.isEmpty()) {
Collection<String> keys = dataList.get(0).keySet();
Collection<Collection<Object>> values = new ArrayList<>();
for (Map<String, Object> recordMap : dataList) {
Collection<Object> recordValues = new ArrayList<>();
for (String key : keys) {
recordValues.add(recordMap.get(key));
}
values.add(recordValues);
}
customMapper.saveBatchToPartition(api.toLowerCase(), partitionName, keys, values);
log.info("批量插入分区 {} 数据 {} 条", partitionName, dataList.size());
}
}
List<String> ids = Arrays.stream(records).map(SObject::getId).collect(Collectors.toList());
List<String> existsIds = customMapper.getIds(api, ids);
String partitionName = null;
if (isPartitioned && batchField != null) {
partitionName = buildPartitionName(batchField, objects);
}
List<Map<String, Object>> insertMaps = new ArrayList<>();
List<Map<String, Object>> updateMaps = new ArrayList<>();
List<String> updateIds = new ArrayList<>();
for (int i = 0; i < objects.size(); i++) {
JSONObject jsonObject = objects.getJSONObject(i);
String id = jsonObject.getString("Id");
Map<String, Object> recordMap = new HashMap<>();
for (String key : jsonObject.keySet()) {
recordMap.put(key, jsonObject.get(key));
}
} else {
if (!normalData.isEmpty()) {
Collection<String> keys = normalData.get(0).keySet();
Collection<Collection<Object>> values = new ArrayList<>();
for (Map<String, Object> recordMap : normalData) {
Collection<Object> recordValues = new ArrayList<>();
for (String key : keys) {
recordValues.add(recordMap.get(key));
}
values.add(recordValues);
}
customMapper.saveBatch(api.toLowerCase(), keys, values);
log.info("批量插入普通表数据 {} 条", normalData.size());
if (existsIds.contains(id)) {
updateMaps.add(recordMap);
updateIds.add(id);
} else {
insertMaps.add(recordMap);
}
}
if (!insertMaps.isEmpty()) {
List<String> keys = new ArrayList<>(insertMaps.get(0).keySet());
List<Collection<Object>> values = new ArrayList<>();
for (Map<String, Object> recordMap : insertMaps) {
List<Object> rowValues = new ArrayList<>();
for (String key : keys) {
rowValues.add(recordMap.get(key));
}
values.add(rowValues);
}
if (isPartitioned && partitionName != null) {
customMapper.saveBatchToPartition(api, partitionName, keys, values);
log.info("成功插入 {} 条记录到分区表 {} 的分区 {}", values.size(), api, partitionName);
} else {
customMapper.saveBatch(api, keys, values);
log.info("成功插入 {} 条记录到表 {}", values.size(), api);
}
count += values.size();
}
if (!updateMaps.isEmpty()) {
for (int i = 0; i < updateMaps.size(); i++) {
Map<String, Object> recordMap = updateMaps.get(i);
String id = updateIds.get(i);
List<Map<String, Object>> maps = new ArrayList<>();
for (Map.Entry<String, Object> entry : recordMap.entrySet()) {
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("key", entry.getKey());
paramMap.put("value", entry.getValue());
maps.add(paramMap);
}
if (isPartitioned && partitionName != null) {
customMapper.updateByIdFromPartition(api, partitionName, maps, id);
} else {
customMapper.updateById(api, maps, id);
}
}
log.info("成功更新 {} 条记录到表 {}", updateMaps.size(), api);
count += updateMaps.size();
}
} catch (Exception e) {
log.error("批量处理数据时发生异常API: {}", api, e);
fallbackToSingleRecordProcessing(api, records, fieldList, isPartitioned, batchField);
log.error("处理API {} 的查询结果时发生异常", api, e);
throw new RuntimeException("处理查询结果失败: " + e.getMessage(), e);
}
return count;
}
/**
* 批量处理失败时的回退方案使用单条记录处理
*
* @param api API名称
* @param records 记录列表
* @param fieldList 字段列表
* @param isPartitioned 是否为分区表
* @param batchField 批次字段
*/
private void fallbackToSingleRecordProcessing(String api, SObject[] records, List<String> fieldList, boolean isPartitioned, String batchField) {
log.warn("批量处理数据失败将回退到单条记录处理API: {}", api);
if (records == null || records.length == 0) {
log.info("回退处理API {} 的记录,共 0 条记录", api);
return;
}
for (SObject record : records) {
if (record == null) {
continue;
}
try {
Map<String, Object> recordMap = convertSObjectToMap(record, fieldList);
if (isPartitioned) {
String partitionName = "p_default";
if (StringUtils.isNotEmpty(batchField) && recordMap.containsKey(batchField.toLowerCase())) {
Object dateValue = recordMap.get(batchField.toLowerCase());
if (dateValue instanceof Date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime((Date) dateValue);
int year = calendar.get(Calendar.YEAR);
partitionName = "p" + year;
}
}
customMapper.upsertToPartition(api.toLowerCase(), partitionName, recordMap);
} else {
customMapper.upsert(api.toLowerCase(), recordMap);
}
} catch (Exception e) {
String recordId = record.getId() != null ? record.getId() : "未知";
log.error("单条处理记录时发生异常记录ID: {}", recordId, e);
}
}
}
/**
* 检查表是否已分区
@ -735,73 +748,27 @@ public class DataiIntegrationBatchServiceImpl implements IDataiIntegrationBatchS
}
/**
* 将SObject转换为Map
* 根据批次字段和数据构建分区名
*
* @param record SObject记录
* @param fieldList 字段列表
* @return 转换后的Map
* @param batchField 批次字段名
* @param objects 数据对象数组
* @return 分区名如p2025
*/
private Map<String, Object> convertSObjectToMap(SObject record, List<String> fieldList) {
Map<String, Object> recordMap = new HashMap<>();
if (record.getId() != null) {
recordMap.put("Id", record.getId());
}
for (String field : fieldList) {
if ("Id".equalsIgnoreCase(field)) {
continue;
}
try {
Object value = record.getField(field);
if (value != null) {
if (value instanceof java.util.Calendar) {
recordMap.put(field.toLowerCase(), ((Calendar) value).getTime());
} else {
recordMap.put(field.toLowerCase(), value);
}
} else {
recordMap.put(field.toLowerCase(), null);
}
} catch (Exception e) {
log.warn("获取字段 {} 的值时发生异常", field, e);
private String buildPartitionName(String batchField, JSONArray objects) {
String partitionName = "p_default";
if (StringUtils.isNotEmpty(batchField) && !objects.isEmpty()) {
JSONObject firstRecord = objects.getJSONObject(0);
Object dateValue = firstRecord.get(batchField);
if (dateValue instanceof Date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime((Date) dateValue);
int year = calendar.get(Calendar.YEAR);
partitionName = "p" + year;
}
}
return recordMap;
}
/**
* Salesforce查询参数内部类
*/
private static class SalesforceParam {
private String api;
private String select;
private Boolean isDeleted;
public String getApi() {
return api;
}
public void setApi(String api) {
this.api = api;
}
public String getSelect() {
return select;
}
public void setSelect(String select) {
this.select = select;
}
public Boolean getIsDeleted() {
return isDeleted;
}
public void setIsDeleted(Boolean isDeleted) {
this.isDeleted = isDeleted;
}
return partitionName;
}
}

View File

@ -11,6 +11,7 @@ import com.datai.common.utils.CacheUtils;
import com.datai.integration.factory.impl.SOAPConnectionFactory;
import com.datai.integration.mapper.CustomMapper;
import com.datai.integration.model.domain.DataiIntegrationBatch;
import com.datai.integration.util.ConvertUtil;
import com.datai.salesforce.common.utils.SoqlBuilder;
import com.sforce.soap.partner.*;
import com.sforce.soap.partner.sobject.SObject;
@ -588,7 +589,7 @@ public class DataiIntegrationMetadataChangeServiceImpl implements IDataiIntegrat
for (com.sforce.soap.partner.Field field : objDetail.getFields()) {
Map<String, Object> fieldDef = new HashMap<>();
fieldDef.put("name", field.getName());
fieldDef.put("type", convertSalesforceTypeToMySQL(field.getType() != null ? field.getType().toString() : null));
fieldDef.put("type", ConvertUtil.fieldTypeToMysql(field));
fieldDef.put("comment", field.getLabel() != null ? field.getLabel().replaceAll("'", "\\\\'") : "");
fieldDefinitions.add(fieldDef);
fields.add(field.getName());
@ -819,7 +820,7 @@ public class DataiIntegrationMetadataChangeServiceImpl implements IDataiIntegrat
for (com.sforce.soap.partner.Field field : objDetail.getFields()) {
if (fieldApi.equals(field.getName())) {
String mysqlType = convertSalesforceTypeToMySQL(field.getType() != null ? field.getType().toString() : null);
String mysqlType = ConvertUtil.fieldTypeToMysql(field);
customMapper.addField(objectApi, fieldApi, mysqlType, field.isNillable());
log.info("成功添加字段: {}.{} 类型: {}", objectApi, fieldApi, mysqlType);
@ -855,7 +856,7 @@ public class DataiIntegrationMetadataChangeServiceImpl implements IDataiIntegrat
for (com.sforce.soap.partner.Field field : objDetail.getFields()) {
if (fieldApi.equals(field.getName())) {
String mysqlType = convertSalesforceTypeToMySQL(field.getType() != null ? field.getType().toString() : null);
String mysqlType = ConvertUtil.fieldTypeToMysql(field);
customMapper.modifyField(objectApi, fieldApi, mysqlType, field.isNillable());
log.info("成功修改字段: {}.{} 类型: {}", objectApi, fieldApi, mysqlType);
@ -888,59 +889,6 @@ public class DataiIntegrationMetadataChangeServiceImpl implements IDataiIntegrat
}
}
private String convertSalesforceTypeToMySQL(String typeStr) {
if (typeStr == null || typeStr.isEmpty()) {
return "VARCHAR(255)";
}
switch (typeStr) {
case "id":
return "VARCHAR(18)";
case "string":
case "email":
case "url":
case "phone":
return "VARCHAR(255)";
case "textarea":
return "TEXT";
case "boolean":
return "TINYINT(1)";
case "int":
return "INT";
case "double":
return "DOUBLE";
case "currency":
return "DECIMAL(18,4)";
case "date":
return "DATE";
case "datetime":
return "DATETIME";
case "time":
return "TIME";
case "percent":
return "DECIMAL(10,2)";
case "reference":
return "VARCHAR(18)";
case "picklist":
case "multipicklist":
return "VARCHAR(255)";
case "combobox":
return "VARCHAR(255)";
case "base64":
return "LONGBLOB";
case "anyType":
return "VARCHAR(255)";
case "address":
return "TEXT";
case "location":
return "VARCHAR(255)";
case "encryptedstring":
return "VARCHAR(255)";
default:
return "VARCHAR(255)";
}
}
@Override
public Map<String, Object> pullAllMetadataChanges() {
// 初始化返回结果
@ -1961,18 +1909,20 @@ public class DataiIntegrationMetadataChangeServiceImpl implements IDataiIntegrat
}
String systemDataStartTimeStr = CacheUtils.get(salesforceConfigCacheManager.getEnvironmentCacheKey(), "salesforce.data.start.time", String.class);
java.time.LocalDate startDate = java.time.LocalDate.now().minusYears(5);
java.time.LocalDateTime startDate = java.time.LocalDateTime.now().minusYears(5);
startDate = startDate.withHour(0).withMinute(0).withSecond(0).withNano(0);
if (systemDataStartTimeStr != null && !systemDataStartTimeStr.trim().isEmpty()) {
try {
startDate = java.time.LocalDate.parse(systemDataStartTimeStr.trim());
java.time.LocalDate startDateLocalDate = java.time.LocalDate.parse(systemDataStartTimeStr.trim());
startDate = startDateLocalDate.atStartOfDay();
log.info("从缓存获取系统数据开始时间: {}", systemDataStartTimeStr);
} catch (Exception e) {
log.warn("解析系统数据开始时间配置失败: {}, 使用默认值", systemDataStartTimeStr, e);
}
}
java.time.LocalDate endDate = java.time.LocalDate.now();
java.time.LocalDateTime endDate = java.time.LocalDateTime.now();
DataiIntegrationBatch batchTemplate = new DataiIntegrationBatch();
batchTemplate.setApi(objectApi);
@ -1984,14 +1934,17 @@ public class DataiIntegrationMetadataChangeServiceImpl implements IDataiIntegrat
batchTemplate.setUpdateTime(DateUtils.getNowDate());
saveBatch(SalesforceConfigConstants.BATCH_TYPE_YEAR,
java.sql.Date.valueOf(startDate),
java.sql.Date.valueOf(endDate),
java.sql.Timestamp.valueOf(startDate),
java.sql.Timestamp.valueOf(endDate),
objectApi,
objectLabel,
batchField,
connection,
batchTemplate);
object.setLastBatchDate(endDate);
dataiIntegrationObjectService.updateDataiIntegrationObject(object);
log.info("对象 {} 批次创建完成", objectApi);
} catch (Exception e) {
log.error("为对象 {} 创建批次时出错: {}", objectApi, e.getMessage(), e);

View File

@ -1,6 +1,5 @@
package com.datai.integration.service.impl;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
@ -13,10 +12,12 @@ import com.datai.integration.mapper.CustomMapper;
import com.datai.integration.mapper.DataiIntegrationObjectMapper;
import com.datai.integration.model.domain.DataiIntegrationBatch;
import com.datai.integration.model.domain.DataiIntegrationObject;
import com.datai.integration.model.param.DataiSyncParam;
import com.datai.integration.service.IDataiIntegrationBatchService;
import com.datai.integration.service.IDataiIntegrationFieldService;
import com.datai.integration.service.IDataiIntegrationObjectService;
import com.datai.salesforce.common.param.SalesforceParam;
import com.datai.salesforce.common.utils.SoqlBuilder;
import com.datai.integration.util.ConvertUtil;
import com.datai.setting.future.SalesforceExecutor;
import com.sforce.soap.partner.DescribeSObjectResult;
import com.sforce.soap.partner.Field;
@ -197,7 +198,7 @@ public class DataiIntegrationObjectServiceImpl implements IDataiIntegrationObjec
data.put("lastSyncEndTime", syncStats.get("lastSyncEndTime"));
data.put("fullSyncCount", syncStats.get("fullSyncCount"));
data.put("incrementalSyncCount", syncStats.get("incrementalSyncCount"));
data.put("lastFullSyncDate", object.getLastFullSyncDate());
data.put("lastBatchDate", object.getLastBatchDate());
data.put("lastSyncDate", object.getLastSyncDate());
data.put("syncStatus", object.getSyncStatus());
data.put("errorMessage", object.getErrorMessage());
@ -585,9 +586,9 @@ public class DataiIntegrationObjectServiceImpl implements IDataiIntegrationObjec
}
}
if (object.getLastFullSyncDate() != null) {
if (latestFullSyncDate == null || object.getLastFullSyncDate().isAfter(latestFullSyncDate)) {
latestFullSyncDate = object.getLastFullSyncDate();
if (object.getLastBatchDate() != null) {
if (latestFullSyncDate == null || object.getLastBatchDate().isAfter(latestFullSyncDate)) {
latestFullSyncDate = object.getLastBatchDate();
}
}
}
@ -634,7 +635,7 @@ public class DataiIntegrationObjectServiceImpl implements IDataiIntegrationObjec
* 同步单个Salesforce对象的数据到本地数据库
*
* @param id 对象ID用于标识需要同步的Salesforce对象
* @return Map<String, Object> 同步结果包含successmessageobjectIdobjectApitotalCountdurationsyncTypelastFullSyncDate等字段
* @return Map<String, Object> 同步结果包含successmessageobjectIdobjectApitotalCountdurationsyncTypelastBatchDate等字段
* @throws ConnectionException 当获取Salesforce连接失败时抛出
* @throws RuntimeException 当同步过程中发生其他异常时抛出
*/
@ -681,14 +682,14 @@ public class DataiIntegrationObjectServiceImpl implements IDataiIntegrationObjec
log.info("准备同步对象数据对象API: {}", objectApi);
LocalDateTime lastFullSyncDate = object.getLastFullSyncDate();
LocalDateTime lastSyncDate = object.getLastSyncDate();
Map<String, Object> syncResult;
if (lastFullSyncDate == null) {
log.info("对象 {} 的全量拉取时间为空,执行全量数据拉取", objectApi);
if (lastSyncDate == null) {
log.info("对象 {} 的最近同步时间为空,执行全量数据拉取", objectApi);
syncResult = syncFullData(object, startTime);
} else {
log.info("对象 {} 的全量拉取时间不为空({}),执行增量数据拉取", objectApi, lastFullSyncDate);
log.info("对象 {} 的最近同步时间不为空({}),执行增量数据拉取", objectApi, lastSyncDate);
syncResult = syncIncrementalData(object, startTime);
}
@ -798,7 +799,6 @@ public class DataiIntegrationObjectServiceImpl implements IDataiIntegrationObjec
long duration = endTime - startTime;
LocalDateTime now = LocalDateTime.now();
object.setLastFullSyncDate(now);
object.setLastSyncDate(now);
object.setTotalRows(totalSyncCount);
object.setSyncStatus(failedBatchCount == 0);
@ -812,7 +812,7 @@ public class DataiIntegrationObjectServiceImpl implements IDataiIntegrationObjec
result.put("totalCount", totalSyncCount);
result.put("duration", duration);
result.put("syncType", "full");
result.put("lastFullSyncDate", object.getLastFullSyncDate());
result.put("lastBatchDate", object.getLastBatchDate());
result.put("totalBatchCount", batches.size());
result.put("successBatchCount", successBatchCount);
result.put("failedBatchCount", failedBatchCount);
@ -840,7 +840,7 @@ public class DataiIntegrationObjectServiceImpl implements IDataiIntegrationObjec
private Map<String, Object> syncIncrementalData(DataiIntegrationObject object, long startTime) {
Map<String, Object> result = new HashMap<>();
String objectApi = object.getApi().trim();
LocalDateTime lastFullSyncDate = object.getLastFullSyncDate();
LocalDateTime lastBatchDate = object.getLastBatchDate();
try {
if (!Boolean.TRUE.equals(object.getIsIncremental())) {
@ -879,7 +879,7 @@ public class DataiIntegrationObjectServiceImpl implements IDataiIntegrationObjec
incrementalBatch.setBatchField(batchField);
incrementalBatch.setSyncType("INCREMENTAL");
incrementalBatch.setSyncStatus(false);
incrementalBatch.setSyncStartDate(lastFullSyncDate);
incrementalBatch.setSyncStartDate(lastBatchDate);
incrementalBatch.setSyncEndDate(LocalDateTime.now());
incrementalBatch.setCreateTime(DateUtils.getNowDate());
incrementalBatch.setUpdateTime(DateUtils.getNowDate());
@ -887,6 +887,9 @@ public class DataiIntegrationObjectServiceImpl implements IDataiIntegrationObjec
int batchId = dataiIntegrationBatchService.insertDataiIntegrationBatch(incrementalBatch);
log.info("为对象 {} 创建增量批次批次ID: {}", objectApi, batchId);
object.setLastBatchDate(incrementalBatch.getSyncEndDate());
updateDataiIntegrationObject(object);
Map<String, Object> batchResult = dataiIntegrationBatchService.syncBatchData(batchId);
long endTime = System.currentTimeMillis();
@ -912,7 +915,7 @@ public class DataiIntegrationObjectServiceImpl implements IDataiIntegrationObjec
result.put("totalCount", syncNum);
result.put("duration", duration);
result.put("syncType", "incremental");
result.put("lastFullSyncDate", object.getLastFullSyncDate());
result.put("lastBatchDate", object.getLastBatchDate());
result.put("lastSyncDate", object.getLastSyncDate());
log.info("对象 {} 增量数据拉取成功,同步数据量: {},耗时: {}ms", objectApi, syncNum, duration);
@ -960,149 +963,6 @@ public class DataiIntegrationObjectServiceImpl implements IDataiIntegrationObjec
return fields;
}
private int executeQueryAndProcessData(PartnerConnection connection, SalesforceParam param, List<String> fieldList)
{
int totalCount = 0;
int failCount = 0;
int batch = 0;
Date lastCreatedDate = null;
String maxId = null;
final int MAX_FAIL_COUNT = 3;
boolean isFirstQuery = true;
try {
while (true) {
try {
String query = buildDynamicQuery(param, lastCreatedDate, maxId, isFirstQuery);
log.info("执行查询,批次: {}, SQL: {}", batch, query);
QueryResult queryResult = connection.query(query);
if (queryResult == null || queryResult.getRecords() == null || queryResult.getRecords().length == 0) {
log.info("批次 {} 查询完成,无更多数据", batch);
break;
}
int currentBatchCount = processQueryResult(param.getApi(), queryResult, fieldList);
totalCount += currentBatchCount;
SObject[] records = queryResult.getRecords();
lastCreatedDate = getLastCreatedDate(records, param.getDateField());
maxId = getMaxId(records, param.getDateField(), lastCreatedDate);
log.info("批次 {} 处理完成,本批次记录数: {}, 总记录数: {}, 最后时间: {}, 最大ID: {}",
batch, currentBatchCount, totalCount, lastCreatedDate, maxId);
failCount = 0;
batch++;
isFirstQuery = false;
}catch (Exception e) {
failCount++;
log.error("批次 {} 查询失败,失败次数: {}/{},错误信息: {}",
batch, failCount, MAX_FAIL_COUNT, e.getMessage(), e);
if (failCount > MAX_FAIL_COUNT) {
log.error("达到最大失败次数 {},停止查询", MAX_FAIL_COUNT);
throw new RuntimeException("查询失败次数过多: " + e.getMessage(), e);
}
}
}
log.info("查询完成,总批次: {}, 总记录数: {}", batch, totalCount);
} catch (Exception e) {
log.error("执行查询并处理数据失败: {}", e.getMessage(), e);
}
return totalCount;
}
/**
* 从查询结果中获取最后一条记录的日期字段值
*
* @param records Salesforce查询返回的记录数组
* @param dateField 日期字段名称用于提取时间戳
* @return 最后一条记录的日期字段值如果获取失败则返回null
*/
private Date getLastCreatedDate(SObject[] records, String dateField)
{
// 检查记录数组是否为空
if (records == null || records.length == 0) {
return null;
}
// 获取最后一条记录
SObject lastRecord = records[records.length - 1];
if (lastRecord == null) {
return null;
}
try {
// 提取指定的日期字段值
Object dateValue = lastRecord.getField(dateField);
if (dateValue instanceof Calendar) {
// 将Calendar类型转换为Date类型
return ((Calendar) dateValue).getTime();
}
} catch (Exception e) {
log.warn("获取最后创建时间失败: {}", e.getMessage());
}
return null;
}
/**
* 从查询结果中获取指定日期对应的最大ID值
*
* @param records Salesforce查询返回的记录数组
* @param dateField 日期字段名称用于比较日期值
* @param lastCreatedDate 日期参考值只查找该日期的记录
* @return 指定日期对应的最大ID值如果没有匹配记录则返回null
*/
private String getMaxId(SObject[] records, String dateField, Date lastCreatedDate)
{
// 检查记录数组和日期值是否为空避免空指针异常
if (records == null || records.length == 0 || lastCreatedDate == null) {
return null;
}
// 初始化最大ID变量用于存储遍历过程中找到的最大ID值
String maxId = null;
// 遍历所有记录查找日期字段值等于lastCreatedDate的记录
for (SObject record : records) {
// 跳过空记录避免空指针异常
if (record == null) {
continue;
}
try {
// 提取记录的日期字段值
Object dateValue = record.getField(dateField);
if (dateValue instanceof Calendar) {
// 将Calendar类型转换为Date类型便于比较
Date recordDate = ((Calendar) dateValue).getTime();
// 只处理与参考日期相同的记录
if (recordDate.equals(lastCreatedDate)) {
// 获取记录的ID
String recordId = record.getId();
// 更新最大ID如果当前记录ID大于已知的最大ID则更新
if (recordId != null && (maxId == null || recordId.compareTo(maxId) > 0)) {
maxId = recordId;
}
}
}
} catch (Exception e) {
// 字段获取失败时记录警告日志继续处理其他记录
log.warn("获取记录ID失败: {}", e.getMessage());
}
}
// 返回指定日期对应的最大ID值该值将作为下一批次查询的起始ID
return maxId;
}
/**
* 构建动态SOQL查询语句
@ -1113,247 +973,47 @@ public class DataiIntegrationObjectServiceImpl implements IDataiIntegrationObjec
* @param isFirstQuery 是否为首次查询
* @return 构建好的SOQL查询语句
*/
private String buildDynamicQuery(SalesforceParam param, Date lastCreatedDate, String maxId, boolean isFirstQuery)
private String buildDynamicQuery(DataiSyncParam param, Date lastCreatedDate, String maxId, boolean isFirstQuery)
{
// 使用StringBuilder构建查询语句提高字符串拼接效率
StringBuilder queryBuilder = new StringBuilder();
// 构建SELECT和FROM子句
queryBuilder.append("SELECT ").append(param.getSelect())
.append(" FROM ").append(param.getApi());
SoqlBuilder builder = new SoqlBuilder();
// 创建条件列表用于存储所有WHERE条件
List<String> conditions = new ArrayList<>();
String[] selectFields = param.getSelect().split(",");
builder.select(selectFields)
.from(param.getApi());
// 如果配置了查询已删除记录添加IsDeleted条件
if (param.getIsDeleted() != null && param.getIsDeleted()) {
conditions.add("IsDeleted = true");
builder.where("IsDeleted = true");
}
// 首次查询时如果配置了开始修改日期添加日期范围条件
if (isFirstQuery && param.getBeginModifyDate() != null) {
String dateField = param.getDateField();
String dateField = param.getBatchField();
if (StringUtils.isNotEmpty(dateField)) {
// 创建UTC时区的日期格式化器
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'.000Z'");
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
// 格式化日期为Salesforce支持的格式
String dateStr = sdf.format(param.getBeginModifyDate());
// 添加日期大于等于条件
conditions.add(dateField + " >= " + dateStr);
builder.whereGe(dateField, param.getBeginModifyDate());
}
}
// 非首次查询时使用最后创建日期作为查询条件
if (!isFirstQuery && lastCreatedDate != null) {
String dateField = param.getDateField();
String dateField = param.getBatchField();
if (StringUtils.isNotEmpty(dateField)) {
// 创建UTC时区的日期格式化器
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'.000Z'");
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
// 格式化日期为Salesforce支持的格式
String dateStr = sdf.format(lastCreatedDate);
// 添加日期大于等于条件
conditions.add(dateField + " >= " + dateStr);
builder.whereGe(dateField, lastCreatedDate);
}
}
// 如果配置了最大ID添加ID大于条件实现精确分页
if (StringUtils.isNotEmpty(maxId)) {
conditions.add("Id > '" + maxId + "'");
builder.whereGt("Id", maxId);
}
// 如果有条件构建WHERE子句
if (!conditions.isEmpty()) {
queryBuilder.append(" WHERE ").append(String.join(" AND ", conditions));
}
builder.orderBy(param.getBatchField(), SoqlBuilder.SortOrder.ASC)
.orderBy("Id", SoqlBuilder.SortOrder.ASC);
// 添加ORDER BY子句按日期字段和ID排序确保数据顺序一致
queryBuilder.append(" ORDER BY ").append(param.getDateField()).append(", Id");
// 如果配置了查询限制添加LIMIT子句
if (param.getLimit() != null && param.getLimit() > 0) {
queryBuilder.append(" LIMIT ").append(param.getLimit());
builder.limit(param.getLimit());
}
// 返回构建好的SOQL查询语句
return queryBuilder.toString();
return builder.build();
}
/**
* 处理查询结果并将数据保存到数据库
*
* @param api Salesforce对象的API名称用于标识目标表
* @param result Salesforce查询返回的结果集包含所有需要同步的记录
* @param fieldList 需要同步的字段列表用于控制数据转换和存储的字段范围
* @return 成功处理的记录数量
*/
private int processQueryResult(String api, QueryResult result, List<String> fieldList)
{
// 初始化计数器用于统计成功处理的记录数量
int count = 0;
// 检查查询结果是否为空避免空指针异常
if (result == null || result.getRecords() == null) {
log.info("处理API {} 的查询结果,共 0 条记录", api);
return count;
}
// 获取查询结果中的所有记录
SObject[] records = result.getRecords();
log.info("处理API {} 的查询结果,共 {} 条记录", api, records.length);
// 检查目标表是否为分区表决定数据保存策略
boolean isPartitioned = checkIfTablePartitioned(api);
// 初始化批次字段变量用于分区表的数据分配
String batchField = null;
// 如果是分区表获取批次字段配置
if (isPartitioned) {
// 根据API名称查询对象配置
DataiIntegrationObject object = dataiIntegrationObjectMapper.selectDataiIntegrationObjectByApi(api);
if (object != null && StringUtils.isNotEmpty(object.getBatchField())) {
// 使用对象配置的批次字段
batchField = object.getBatchField();
log.info("对象 {} 配置了批次字段: {}", api, batchField);
} else {
// 如果对象未配置批次字段使用日期字段作为默认批次字段
batchField = dataiIntegrationFieldService.getDateField(api);
log.info("对象 {} 未配置批次字段,使用日期字段: {}", api, batchField);
}
}
// 创建分区数据Map用于存储按分区名分组的数据
Map<String, List<Map<String, Object>>> partitionedData = new HashMap<>();
// 创建普通数据列表用于存储非分区表的数据
List<Map<String, Object>> normalData = new ArrayList<>();
// 遍历所有记录进行数据转换和分区分配
for (SObject record : records) {
// 跳过空记录避免空指针异常
if (record == null) {
continue;
}
try {
// 将SObject对象转换为Map格式便于后续处理
Map<String, Object> recordMap = convertSObjectToMap(record, fieldList);
// 如果是分区表且配置了批次字段进行分区分配
if (isPartitioned && StringUtils.isNotEmpty(batchField)) {
// 初始化分区名称为默认分区
String partitionName = "p_default";
// 检查记录中是否包含批次字段
if (recordMap.containsKey(batchField.toLowerCase())) {
// 获取批次字段的值
Object batchValue = recordMap.get(batchField.toLowerCase());
if (batchValue instanceof Date) {
// 如果批次字段值为Date类型使用年份作为分区标识如p2025
Calendar calendar = Calendar.getInstance();
calendar.setTime((Date) batchValue);
int year = calendar.get(Calendar.YEAR);
partitionName = "p" + year;
log.debug("记录根据批次字段 {} 的值 {} 分配到分区 {}", batchField, batchValue, partitionName);
} else if (batchValue != null) {
// 如果批次字段值为其他类型使用哈希值作为分区标识
partitionName = "p_" + String.valueOf(batchValue).hashCode();
log.debug("记录根据批次字段 {} 的值 {} 分配到分区 {}", batchField, batchValue, partitionName);
}
}
// 将记录添加到对应分区的数据列表中
partitionedData.computeIfAbsent(partitionName, k -> new ArrayList<>()).add(recordMap);
} else {
// 非分区表直接添加到普通数据列表
normalData.add(recordMap);
}
// 增加处理计数
count++;
} catch (Exception e) {
// 记录处理异常但继续处理其他记录
String recordId = record.getId() != null ? record.getId() : "未知";
log.error("处理记录时发生异常记录ID: {}", recordId, e);
}
}
try {
// 如果是分区表且有分区数据批量保存到各个分区
if (isPartitioned && !partitionedData.isEmpty()) {
log.info("开始批量保存分区数据,共 {} 个分区", partitionedData.size());
// 遍历所有分区保存数据
for (Map.Entry<String, List<Map<String, Object>>> entry : partitionedData.entrySet()) {
String partitionName = entry.getKey();
List<Map<String, Object>> dataList = entry.getValue();
// 保存数据到指定分区
saveDataToPartition(api, partitionName, dataList);
log.info("成功保存 {} 条记录到分区 {}", dataList.size(), partitionName);
}
} else if (!normalData.isEmpty()) {
// 非分区表直接保存数据到目标表
saveDataToTable(api, normalData);
log.info("成功保存 {} 条记录到表 {}", normalData.size(), api);
}
} catch (Exception e) {
// 记录批量保存失败的异常
log.error("批量保存数据到数据库失败: {}", e.getMessage(), e);
}
// 返回成功处理的记录数量
return count;
}
/**
* 将Salesforce的SObject对象转换为Map格式
*
* @param record Salesforce的SObject对象包含从Salesforce查询返回的单条记录数据
* @param fieldList 需要提取的字段列表控制转换过程中包含哪些字段
* @return 包含所有指定字段值的Map对象字段名统一为小写格式
*/
private Map<String, Object> convertSObjectToMap(SObject record, List<String> fieldList)
{
// 创建Map对象用于存储转换后的记录数据
Map<String, Object> recordMap = new HashMap<>();
// 添加记录ID字段Id字段总是被包含在结果中
if (record.getId() != null) {
recordMap.put("Id", record.getId());
}
// 遍历所有需要提取的字段
for (String field : fieldList) {
// 跳过Id字段因为已经单独处理过
if ("Id".equalsIgnoreCase(field)) {
continue;
}
try {
// 从SObject中获取字段值
Object value = record.getField(field);
if (value != null) {
// 检查字段值是否为Calendar类型日期类型
if (value instanceof java.util.Calendar) {
// 将Calendar类型转换为Date类型便于数据库存储
recordMap.put(field.toLowerCase(), ((Calendar) value).getTime());
} else {
// 其他类型直接存储字段名转换为小写以适应数据库命名规范
recordMap.put(field.toLowerCase(), value);
}
} else {
// 空值字段也保留在结果中值为null
recordMap.put(field.toLowerCase(), null);
}
} catch (Exception e) {
// 字段获取异常时记录警告日志但不中断处理流程
log.warn("获取字段 {} 的值时发生异常", field, e);
}
}
// 返回转换后的Map对象
return recordMap;
}
/**
* 检查指定表是否为分区表
*
@ -1363,11 +1023,13 @@ public class DataiIntegrationObjectServiceImpl implements IDataiIntegrationObjec
private boolean checkIfTablePartitioned(String tableName)
{
try {
// 调用CustomMapper查询数据库元数据判断表是否为分区表
return customMapper.isPartitioned(tableName);
DataiIntegrationObject object = dataiIntegrationObjectMapper.selectDataiIntegrationObjectByApi(tableName);
if (object != null && object.getIsPartitioned() != null) {
return object.getIsPartitioned();
}
return false;
} catch (Exception e) {
// 查询失败时记录警告日志默认按非分区表处理
log.warn("检查表 {} 是否分区失败: {}", tableName, e.getMessage());
log.warn("检查对象 {} 分区状态失败: {},默认按非分区表处理", tableName, e.getMessage());
return false;
}
}

View File

@ -5,6 +5,7 @@ import com.datai.integration.model.domain.DataiIntegrationFilterLookup;
import com.datai.integration.model.domain.DataiIntegrationMetadataChange;
import com.datai.integration.model.domain.DataiIntegrationObject;
import com.datai.integration.model.domain.DataiIntegrationPicklist;
import com.datai.integration.model.param.DataiSyncParam;
import com.datai.integration.factory.impl.SOAPConnectionFactory;
import com.datai.integration.mapper.CustomMapper;
import com.datai.integration.service.IDataiIntegrationFieldService;
@ -13,7 +14,8 @@ import com.datai.integration.service.IDataiIntegrationMetadataChangeService;
import com.datai.integration.service.IDataiIntegrationObjectService;
import com.datai.integration.service.IDataiIntegrationPicklistService;
import com.datai.integration.service.ISalesforceDataPullService;
import com.datai.salesforce.common.param.SalesforceParam;
import com.datai.salesforce.common.utils.SoqlBuilder;
import com.datai.integration.util.ConvertUtil;
import com.datai.setting.future.SalesforceExecutor;
import com.sforce.soap.partner.DescribeGlobalResult;
import com.sforce.soap.partner.DescribeGlobalSObjectResult;
@ -661,7 +663,7 @@ public class SalesforceDataPullServiceImpl implements ISalesforceDataPullService
log.info("获取到对象 {} 的字段列表,共 {} 个字段", objectApi, fieldList.size());
// 构建查询参数
SalesforceParam param = new SalesforceParam();
DataiSyncParam param = new DataiSyncParam();
param.setApi(objectApi);
param.setSelect(String.join(",", fieldList));
@ -737,7 +739,7 @@ public class SalesforceDataPullServiceImpl implements ISalesforceDataPullService
* @param fieldList 字段列表
* @return 处理的数据条数
*/
private int executeQueryAndProcessData(PartnerConnection connection, SalesforceParam param, List<String> fieldList) {
private int executeQueryAndProcessData(PartnerConnection connection, DataiSyncParam param, List<String> fieldList) {
int totalCount = 0;
try {
// 构建查询语句
@ -780,18 +782,55 @@ public class SalesforceDataPullServiceImpl implements ISalesforceDataPullService
* @param param 查询参数
* @return SOQL查询语句
*/
private String buildDynamicQuery(SalesforceParam param) {
// 构建简单的查询语句实际实现需要根据实际情况调整
StringBuilder queryBuilder = new StringBuilder();
queryBuilder.append("SELECT ").append(param.getSelect())
.append(" FROM ").append(param.getApi());
private String buildDynamicQuery(DataiSyncParam param) {
SoqlBuilder builder = new SoqlBuilder();
String[] selectFields = param.getSelect().split(",");
builder.select(selectFields)
.from(param.getApi());
// 添加删除记录查询条件
if (param.getIsDeleted() != null && param.getIsDeleted()) {
queryBuilder.append(" WHERE IsDeleted = true");
builder.where("IsDeleted = true");
} else if (param.getIsDeleted() != null && !param.getIsDeleted()) {
builder.where("IsDeleted = false");
}
return queryBuilder.toString();
if (param.getBeginDate() != null && param.getEndDate() != null) {
builder.whereGe(param.getBatchField(), param.getBeginDate())
.whereLe(param.getBatchField(), param.getEndDate());
} else if (param.getBeginDate() != null) {
builder.whereGe(param.getBatchField(), param.getBeginDate());
} else if (param.getEndDate() != null) {
builder.whereLe(param.getBatchField(), param.getEndDate());
}
if (param.getBeginCreateDate() != null && param.getEndCreateDate() != null) {
builder.whereGe("CreatedDate", param.getBeginCreateDate())
.whereLe("CreatedDate", param.getEndCreateDate());
} else if (param.getBeginCreateDate() != null) {
builder.whereGe("CreatedDate", param.getBeginCreateDate());
} else if (param.getEndCreateDate() != null) {
builder.whereLe("CreatedDate", param.getEndCreateDate());
}
if (param.getBeginModifyDate() != null && param.getEndModifyDate() != null) {
builder.whereGe("LastModifiedDate", param.getBeginModifyDate())
.whereLe("LastModifiedDate", param.getEndModifyDate());
} else if (param.getBeginModifyDate() != null) {
builder.whereGe("LastModifiedDate", param.getBeginModifyDate());
} else if (param.getEndModifyDate() != null) {
builder.whereLe("LastModifiedDate", param.getEndModifyDate());
}
if (StringUtils.isNotEmpty(param.getMaxId())) {
builder.whereGt(param.getIdField(), param.getMaxId());
}
if (param.getLimit() != null && param.getLimit() > 0) {
builder.limit(param.getLimit());
}
return builder.build();
}
/**
@ -991,42 +1030,7 @@ public class SalesforceDataPullServiceImpl implements ISalesforceDataPullService
* @return 转换后的Map
*/
private Map<String, Object> convertSObjectToMap(SObject record, List<String> fieldList) {
Map<String, Object> recordMap = new HashMap<>();
// 添加记录ID
if (record.getId() != null) {
recordMap.put("Id", record.getId());
}
// 添加其他字段
for (String field : fieldList) {
// 跳过ID字段因为已经处理过了
if ("Id".equalsIgnoreCase(field)) {
continue;
}
try {
Object value = record.getField(field);
// 处理不同类型的字段值
if (value != null) {
// 处理日期类型
if (value instanceof java.util.Calendar) {
recordMap.put(field.toLowerCase(), ((Calendar) value).getTime());
}
// 处理其他类型
else {
recordMap.put(field.toLowerCase(), value);
}
} else {
// 对于null值可以选择不放入map或者放入null
recordMap.put(field.toLowerCase(), null);
}
} catch (Exception e) {
log.warn("获取字段 {} 的值时发生异常", field, e);
}
}
return recordMap;
return null;
}
@ -1158,7 +1162,7 @@ public class SalesforceDataPullServiceImpl implements ISalesforceDataPullService
log.info("获取到对象 {} 的字段列表: {}", objectApi, fieldList);
// 构建查询参数
SalesforceParam param = new SalesforceParam();
DataiSyncParam param = new DataiSyncParam();
param.setApi(objectApi);
param.setSelect(String.join(",", fieldList));

View File

@ -0,0 +1,756 @@
package com.datai.integration.util;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.datai.salesforce.common.constant.SalesforceConstants;
import com.sforce.soap.partner.Field;
import com.sforce.soap.partner.sobject.SObject;
import com.sforce.ws.bind.XmlObject;
import com.sforce.ws.types.Time;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.map.CaseInsensitiveMap;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.commons.lang3.time.FastDateFormat;
import org.apache.commons.lang3.time.FastTimeZone;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.*;
/**
* Salesforce到MySQL数据转换工具类
*
* 主要功能
* 1. 将Salesforce的SObject对象转换为Map
* 2. 处理各种数据类型的转换确保与MySQL数据库兼容
* 3. 过滤和转换复杂类型字段
* 4. 字段类型映射Salesforce -> MySQL
* 5. 日期时间处理和转换
* 6. 时间范围计算
* 7. 任务分割
*
* 支持的类型转换
* - Calendar -> Date: 日期类型转换
* - Boolean -> Integer(0/1): 布尔值转换为整数兼容MySQL的TINYINT
* - XmlObjectWrapper -> String: 复杂字段转换为字符串
* - 其他类型 -> 保持原样
*/
@Component
@Slf4j
public class ConvertUtil {
private static final String XML_OBJECT_WRAPPER_CLASS = "com.sforce.ws.bind.XmlObjectWrapper";
private static final FastDateFormat SF_DATE_FORMAT = FastDateFormat.getInstance(SalesforceConstants.SF_DATE_FORMAT, FastTimeZone.getGmtTimeZone());
private static final FastDateFormat SF_TIME_FORMAT = FastDateFormat.getInstance(SalesforceConstants.SF_TIME_FORMAT, FastTimeZone.getGmtTimeZone());
private static final FastDateFormat SF_DATE_FORMAT_BULK = FastDateFormat.getInstance(SalesforceConstants.SF_DATE_FORMAT_BULK, FastTimeZone.getGmtTimeZone());
private static final String MYSQL_DATE_FORMAT = "yyyy-MM-dd";
private static final String MYSQL_DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
private static final int MINUTE = 60;
private static final int HOUR = 24;
private ConvertUtil() {
}
/**
* 添加ID字段到Map
*
* @param record SObject对象
* @param recordMap 目标Map
*/
private static void addIdField(SObject record, Map<String, Object> recordMap) {
if (record.getId() != null) {
recordMap.put("Id", record.getId());
}
}
/**
* sObject 数组转 JSONArray
*
* @param records sObject 数组
* @return JSONArray
*/
public static JSONArray toJsonArray(SObject[] records, Field[] fields) throws ParseException {
JSONArray jsonArray = new JSONArray();
for (SObject sObject : records) {
jsonArray.add(toJsonObject(sObject, fields));
}
return jsonArray;
}
/**
* sObject 数组转 JSONObject
*
* @param sObject sObject
* @param fields 字段
* @return JSONObject
*/
public static JSONObject toJsonObject(SObject sObject, Field[] fields) throws ParseException {
Iterator<XmlObject> children = sObject.getChildren();
JSONObject jsonObject = new JSONObject();
CaseInsensitiveMap map = new CaseInsensitiveMap();
for (Field field : fields) {
map.put(field.getName(), field.getType().toString());
}
while (children.hasNext()) {
XmlObject next = children.next();
String name = next.getName().getLocalPart();
Object value = next.getValue();
// 全部当string处理
if (next instanceof SObject) {
SObject sObjectChild = (SObject) next;
Iterator<XmlObject> children1 = sObjectChild.getChildren();
while (children1.hasNext()) {
XmlObject childNext = children1.next();
String childName = childNext.getName().getLocalPart();
Object childValue = childNext.getValue();
// partner会多一个type 值为对象名称 这里过滤掉
if ("type".equals(childName)) {
continue;
}
if (childValue == null) {
continue;
}
jsonObject.put(name+"_"+childName, childValue);
}
}
// partner会多一个type 值为对象名称 这里过滤掉
if ("type".equals(name)) {
continue;
}
if (value == null) {
continue;
}
String type = (String) map.get(name);
if (type == null) {
continue;
}
switch (type) {
case "date":
jsonObject.put(name, DateUtils.parseDate((String) value, "yyyy-MM-dd"));
break;
case "datetime":
jsonObject.put(name, SF_DATE_FORMAT.parse((String) value));
break;
case "time":
jsonObject.put(name, SF_TIME_FORMAT.parse((String) value));
break;
case "boolean":
jsonObject.put(name, BooleanUtils.toBoolean((String) value));
break;
case "long":
case "int":
jsonObject.put(name, Integer.parseInt((String) value));
break;
case "double":
case "currency":
case "percent":
jsonObject.put(name, Double.parseDouble((String) value));
break;
case "base64":
// 文件不处理 统一存oss或者server
break;
default:
if (value instanceof GregorianCalendar) {
jsonObject.put(name, DateFormatUtils.format(((GregorianCalendar) value), "yyyy-MM-dd HH:mm:ss"));
} else if (value instanceof Time){
jsonObject.put(name, DateFormatUtils.format((((Time) value).getTimeInMillis()), "HH:mm:ss"));
}else {
jsonObject.put(name, String.valueOf(value));
}
break;
}
}
return jsonObject;
}
/**
* 转换Calendar为Date
*
* @param calendar Calendar对象
* @return Date对象
*/
private static Date convertCalendarToDate(Calendar calendar) {
return calendar.getTime();
}
/**
* 转换Boolean为Integer
*
* @param bool 布尔值
* @return 整数true=1, false=0
*/
private static Integer convertBooleanToInteger(Boolean bool) {
return bool ? 1 : 0;
}
/**
* 判断是否为XmlObjectWrapper类型
*
* @param value 对象
* @return true如果是XmlObjectWrapper类型
*/
private static boolean isXmlObjectWrapper(Object value) {
return XML_OBJECT_WRAPPER_CLASS.equals(value.getClass().getName());
}
/**
* 转换XmlObjectWrapper为String
*
* @param field 字段名
* @param value XmlObjectWrapper对象
* @return 字符串表示
*/
private static String convertXmlObjectWrapperToString(String field, Object value) {
log.warn("跳过复杂字段 {},类型为 XmlObjectWrapper转换为字符串", field);
return String.valueOf(value);
}
/**
* 检查字段是否为复杂类型
*
* @param value 字段值
* @return true如果是复杂类型
*/
public static boolean isComplexType(Object value) {
if (value == null) {
return false;
}
return isXmlObjectWrapper(value);
}
/**
* 检查字段是否需要特殊转换
*
* @param value 字段值
* @return true如果需要转换
*/
public static boolean needsConversion(Object value) {
if (value == null) {
return false;
}
return value instanceof java.util.Calendar
|| value instanceof Boolean
|| value instanceof Time
|| isComplexType(value);
}
/**
* 将Salesforce字段类型转换为MySQL字段类型
*
* @param field Salesforce字段对象
* @return MySQL字段类型
*/
public static String fieldTypeToMysql(Field field) {
String type = field.getType().toString();
int length = field.getLength();
String result = type;
switch (type) {
case "id":
case "reference":
result = "varchar(" + length + ")";
break;
case "string":
if ("Name".equalsIgnoreCase(field.getName())) {
result = "varchar(" + length + ")";
} else {
result = "text";
}
break;
case "url":
case "email":
case "phone":
case "picklist":
case "textarea":
case "multipicklist":
case "address":
case "combobox":
case "location":
case "encryptedstring":
case "anyType":
result = "text";
break;
case "date":
result = "date";
break;
case "datetime":
result = "datetime";
break;
case "boolean":
result = "tinyint(1)";
break;
case "time":
result = "time(3)";
break;
case "long":
case "int":
result = "int(" + length + ")";
break;
case "double":
case "currency":
case "percent":
if (length > 4) {
result = "double(" + length + ")";
} else {
result = "decimal(40,20)";
}
break;
case "base64":
result = "longblob";
break;
default:
result = "text";
break;
}
return result;
}
/**
* 计算两个时间差
*
* @param date1 日期1
* @param date2 日期2
* @return 返回日期格式 xxdxxhxxmin
*/
public static String calTime(Date date1, Date date2) {
if (date1 == null || date2 == null) {
return null;
}
long min = Math.abs(date2.getTime() - date1.getTime()) / 1000 / 60;
StringBuilder stringBuilder = new StringBuilder();
if (min >= MINUTE) {
long h = min / MINUTE;
long realMin = min % MINUTE;
if (h >= HOUR) {
stringBuilder.append(h / HOUR).append("d");
long realH = h % HOUR;
if (realH > 0) {
stringBuilder.append(realH).append("h");
}
} else {
stringBuilder.append(h).append("h");
}
if (realMin > 0) {
stringBuilder.append(realMin).append("min");
}
} else {
stringBuilder.append(min).append("min");
}
return stringBuilder.toString();
}
/**
* 返回本周最后一天 如果大于结束日期 返回结束日期
*
* @param endDate 结束日期
* @param startDate 开始日期
* @return 本周最后一天
*/
public static Date getWeekLastDay(Date endDate, Date startDate) {
Date weekLastDay;
Calendar cal = DateUtils.toCalendar(startDate);
cal.set(Calendar.DAY_OF_WEEK, cal.getActualMaximum(Calendar.DAY_OF_WEEK));
cal.add(Calendar.DAY_OF_WEEK, 1);
weekLastDay = cal.getTime();
if (weekLastDay.compareTo(endDate) > 0) {
weekLastDay = endDate;
}
return weekLastDay;
}
/**
* 返回本月最后一天 如果大于结束日期 返回结束日期
*
* @param endDate 结束日期
* @param startDate 开始日期
* @return 本月最后一天
*/
public static Date getMonthLastDay(Date endDate, Date startDate) {
Date monthLastDay;
Calendar cal = DateUtils.toCalendar(startDate);
cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
cal.add(Calendar.DAY_OF_MONTH, 1);
monthLastDay = cal.getTime();
if (monthLastDay.compareTo(endDate) > 0) {
monthLastDay = endDate;
}
return monthLastDay;
}
/**
* 返回本年最后一天 如果大于结束日期 返回结束日期
*
* @param endDate 结束日期
* @param startDate 开始日期
* @return 本年最后一天
*/
public static Date getYearLastDay(Date endDate, Date startDate) {
Date yearLastDay;
startDate = DateUtils.addYears(startDate, 1);
Calendar cal = DateUtils.toCalendar(startDate);
cal.set(Calendar.YEAR, cal.get(Calendar.YEAR));
yearLastDay = cal.getTime();
if (yearLastDay.compareTo(endDate) > 0) {
yearLastDay = endDate;
}
return yearLastDay;
}
/**
* 根据字段类型将值转换为MySQL类型并返回
*
* @param type 字段类型
* @param value 字段值
* @return 转换后的值
* @throws ParseException 日期解析异常
*/
public static Object convertMysqlValue(String type, Object value) throws ParseException {
return convertMysqlValueInternal(type, value, false);
}
/**
* 根据字段类型将值转换为MySQL类型并返回Bulk API专用
*
* @param type 字段类型
* @param value 字段值
* @return 转换后的值
* @throws ParseException 日期解析异常
*/
public static Object convertMysqlBulkValue(String type, Object value) throws ParseException {
return convertMysqlValueInternal(type, value, true);
}
private static Object convertMysqlValueInternal(String type, Object value, boolean isBulk) throws ParseException {
if (value == null) {
return null;
}
String strValue = value instanceof String ? (String) value : null;
switch (type) {
case "date":
if (StringUtils.isEmpty(strValue)) {
return null;
}
return DateUtils.parseDate(strValue, MYSQL_DATE_FORMAT);
case "datetime":
if (StringUtils.isEmpty(strValue)) {
return null;
}
if (isBulk) {
return SF_DATE_FORMAT_BULK.parse(strValue);
} else {
return SF_DATE_FORMAT.parse(strValue);
}
case "time":
if (StringUtils.isEmpty(strValue)) {
return null;
}
return SF_TIME_FORMAT.parse(strValue);
case "boolean":
if (StringUtils.isEmpty(strValue)) {
return false;
}
return Boolean.parseBoolean(strValue);
case "long":
case "int":
if (StringUtils.isEmpty(strValue)) {
return 0;
}
return Integer.parseInt(strValue);
case "double":
case "currency":
case "percent":
if (StringUtils.isEmpty(strValue)) {
return 0.0;
}
return Double.parseDouble(strValue);
case "base64":
return null;
default:
if (value instanceof GregorianCalendar) {
return DateFormatUtils.format(((GregorianCalendar) value), "yyyy-MM-dd HH:mm:ss");
} else if (value instanceof Time) {
return DateFormatUtils.format((((Time) value).getTimeInMillis()), "HH:mm:ss");
} else {
return String.valueOf(value);
}
}
}
/**
* 将本地数据转换为Salesforce数据Partner API
*
* @param fieldType 字段类型
* @param data 数据值
* @return 转换后的值
* @throws ParseException 解析异常
*/
public static Object localDataToSfData(String fieldType, String data) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
SimpleDateFormat sd = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
Date date;
Calendar calendar = Calendar.getInstance();
switch (fieldType) {
case "int":
return Integer.parseInt(data);
case "double":
case "currency":
case "percent":
return new BigDecimal(data);
case "boolean":
return Boolean.parseBoolean(data);
case "date":
try {
date = sd.parse(data);
calendar.setTime(date);
return calendar;
} catch (ParseException e) {
log.error("日期解析失败: {}", data, e);
return null;
}
case "datetime":
try {
date = sd.parse(data);
calendar.setTime(date);
return calendar;
} catch (ParseException e) {
log.error("日期时间解析失败: {}", data, e);
return null;
}
case "time":
return data;
default:
return data;
}
}
/**
* 将本地数据转换为Salesforce数据Bulk API
*
* @param fieldType 字段类型
* @param data 数据值
* @return 转换后的值
* @throws ParseException 解析异常
*/
public static Object localBulkDataToSfData(String fieldType, String data) throws ParseException {
switch (fieldType) {
case "int":
return Integer.parseInt(data);
case "double":
case "currency":
case "percent":
return new BigDecimal(data);
case "boolean":
return Boolean.parseBoolean(data);
case "date":
return data + "T08:00:00Z";
case "datetime":
LocalDateTime localDateTime = LocalDateTime.parse(data);
LocalDateTime plusEightHours = localDateTime.plusHours(-8);
OffsetDateTime offsetDateTime = plusEightHours.atOffset(ZoneOffset.ofHours(0));
return offsetDateTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
case "time":
return adjustHour(data);
default:
return data;
}
}
/**
* 调整时间格式
*
* @param time 时间字符串
* @return 调整后的时间字符串
*/
private static String adjustHour(String time) {
if (StringUtils.isBlank(time)) {
return time;
}
return time;
}
/**
* 格式化日期时间为MySQL格式
*
* @param date 日期对象
* @return MySQL格式日期时间字符串
*/
public static String formatDateToMysql(Date date) {
if (date == null) {
return null;
}
SimpleDateFormat sdf = new SimpleDateFormat(MYSQL_DATETIME_FORMAT);
return sdf.format(date);
}
/**
* 解析MySQL日期字符串
*
* @param dateStr MySQL日期字符串
* @return Date对象
* @throws ParseException 解析异常
*/
public static Date parseMysqlDate(String dateStr) throws ParseException {
if (StringUtils.isBlank(dateStr)) {
return null;
}
SimpleDateFormat sdf = new SimpleDateFormat(MYSQL_DATETIME_FORMAT);
return sdf.parse(dateStr);
}
/**
* 获取当天的开始时间00:00:00
*
* @param date 日期对象
* @return 当天开始时间
*/
public static Date getStartOfDay(Date date) {
if (date == null) {
return null;
}
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
return calendar.getTime();
}
/**
* 获取当天的结束时间23:59:59
*
* @param date 日期对象
* @return 当天结束时间
*/
public static Date getEndOfDay(Date date) {
if (date == null) {
return null;
}
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.HOUR_OF_DAY, 23);
calendar.set(Calendar.MINUTE, 59);
calendar.set(Calendar.SECOND, 59);
calendar.set(Calendar.MILLISECOND, 999);
return calendar.getTime();
}
/**
* 判断日期是否在指定范围内
*
* @param date 待判断的日期
* @param start 开始日期
* @param end 结束日期
* @return true如果在范围内
*/
public static boolean isDateInRange(Date date, Date start, Date end) {
if (date == null) {
return false;
}
if (start != null && date.before(start)) {
return false;
}
if (end != null && date.after(end)) {
return false;
}
return true;
}
/**
* 添加天数到日期
*
* @param date 日期对象
* @param days 天数
* @return 新日期
*/
public static Date addDays(Date date, int days) {
if (date == null) {
return null;
}
return DateUtils.addDays(date, days);
}
/**
* 添加小时到日期
*
* @param date 日期对象
* @param hours 小时数
* @return 新日期
*/
public static Date addHours(Date date, int hours) {
if (date == null) {
return null;
}
return DateUtils.addHours(date, hours);
}
/**
* 添加分钟到日期
*
* @param date 日期对象
* @param minutes 分钟数
* @return 新日期
*/
public static Date addMinutes(Date date, int minutes) {
if (date == null) {
return null;
}
return DateUtils.addMinutes(date, minutes);
}
/**
* 比较两个日期是否是同一天
*
* @param date1 日期1
* @param date2 日期2
* @return true如果是同一天
*/
public static boolean isSameDay(Date date1, Date date2) {
if (date1 == null || date2 == null) {
return false;
}
return DateUtils.isSameDay(date1, date2);
}
/**
* 截断日期到指定精度
*
* @param date 日期对象
* @param field 精度字段Calendar.DAY_OF_MONTH, Calendar.HOUR_OF_DAY等
* @return 截断后的日期
*/
public static Date truncate(Date date, int field) {
if (date == null) {
return null;
}
return DateUtils.truncate(date, field);
}
}

View File

@ -7,8 +7,8 @@
### 1.2 接口描述
该接口用于触发指定对象的单次数据同步操作将Salesforce对象的数据同步到本地数据库。同步操作包括全量同步和增量同步两种模式具体模式由对象的配置决定
- **全量同步**:当对象的全量拉取时间lastFullSyncDate为空时执行全量数据拉取查询所有全量同步批次并使用多线程并行拉取
- **增量同步**:当对象的全量拉取时间不为空时,执行增量数据拉取,创建增量批次并拉取增量数据
- **全量同步**:当对象的最后批次时间lastBatchDate为空时执行全量数据拉取查询所有全量同步批次并使用多线程并行拉取
- **增量同步**:当对象的最后批次时间不为空时,执行增量数据拉取,创建增量批次并拉取增量数据
### 1.3 接口地址
```
@ -88,7 +88,7 @@ fetch('http://localhost:8080/integration/object/1/syncData', {
"totalCount": 12345,
"duration": 15234,
"syncType": "full",
"lastFullSyncDate": "2025-12-27T10:30:00",
"lastBatchDate": "2025-12-27T10:30:00",
"totalBatchCount": 5,
"successBatchCount": 5,
"failedBatchCount": 0
@ -117,7 +117,7 @@ fetch('http://localhost:8080/integration/object/1/syncData', {
| totalCount | Integer | 同步的总记录数 |
| duration | Long | 同步耗时(毫秒) |
| syncType | String | 同步类型,"full"表示全量同步,"incremental"表示增量同步 |
| lastFullSyncDate | LocalDateTime | 最后全量同步时间 |
| lastBatchDate | LocalDateTime | 最后批次时间 |
| lastSyncDate | LocalDateTime | 最后同步时间(增量同步时返回) |
| totalBatchCount | Integer | 总批次数量(全量同步时返回) |
| successBatchCount | Integer | 成功批次数量(全量同步时返回) |
@ -138,7 +138,7 @@ fetch('http://localhost:8080/integration/object/1/syncData', {
"totalCount": 12345,
"duration": 15234,
"syncType": "full",
"lastFullSyncDate": "2025-12-27T10:30:00",
"lastBatchDate": "2025-12-27T10:30:00",
"totalBatchCount": 5,
"successBatchCount": 5,
"failedBatchCount": 0
@ -159,7 +159,7 @@ fetch('http://localhost:8080/integration/object/1/syncData', {
"totalCount": 234,
"duration": 3456,
"syncType": "incremental",
"lastFullSyncDate": "2025-12-26T10:30:00",
"lastBatchDate": "2025-12-26T10:30:00",
"lastSyncDate": "2025-12-27T10:30:00"
}
}
@ -178,7 +178,7 @@ fetch('http://localhost:8080/integration/object/1/syncData', {
"totalCount": 10000,
"duration": 12000,
"syncType": "full",
"lastFullSyncDate": "2025-12-27T10:30:00",
"lastBatchDate": "2025-12-27T10:30:00",
"totalBatchCount": 5,
"successBatchCount": 4,
"failedBatchCount": 1
@ -280,14 +280,14 @@ fetch('http://localhost:8080/integration/object/1/syncData', {
- 检查对象是否存在
- 检查对象API是否配置
- 检查对象是否启用同步
4. **同步模式判断**:判断对象的全量拉取时间lastFullSyncDate是否为空
4. **同步模式判断**:判断对象的最后批次时间lastBatchDate是否为空
5. **全量同步执行**
- 查询所有 `syncType="FULL"` 的批次
- 使用 `SalesforceExecutor` 线程池多线程并行拉取多个批次
- 统计成功和失败的批次数量
- 统计总同步记录数
6. **对象状态更新**
- 更新对象的 `lastFullSyncDate` 为当前时间
- 更新对象的 `lastBatchDate` 为当前时间
- 更新对象的 `lastSyncDate` 为当前时间
- 更新对象的 `totalRows` 为总记录数
- 更新对象的 `syncStatus`全部成功为true否则为false
@ -301,7 +301,7 @@ fetch('http://localhost:8080/integration/object/1/syncData', {
- 检查对象是否存在
- 检查对象API是否配置
- 检查对象是否启用同步
4. **同步模式判断**:判断对象的全量拉取时间lastFullSyncDate是否为空
4. **同步模式判断**:判断对象的最后批次时间lastBatchDate是否为空
5. **增量同步执行**
- 检查对象是否启用增量更新isIncremental
- 获取Salesforce连接
@ -321,8 +321,8 @@ fetch('http://localhost:8080/integration/object/1/syncData', {
| 条件 | 同步模式 | 说明 |
|------|----------|------|
| lastFullSyncDate == null | 全量同步 | 对象首次同步,执行全量数据拉取 |
| lastFullSyncDate != null | 增量同步 | 对象已执行过全量同步,执行增量数据拉取 |
| lastBatchDate == null | 全量同步 | 对象首次同步,执行全量数据拉取 |
| lastBatchDate != null | 增量同步 | 对象已执行过全量同步,执行增量数据拉取 |
### 4.3 全量同步特性
@ -607,7 +607,7 @@ POST /integration/batch/{id}/syncData
| isWork | Boolean | 是否启用同步 |
| isIncremental | Boolean | 是否启用增量更新 |
| batchField | String | 批次字段 |
| lastFullSyncDate | LocalDateTime | 最后全量同步时间 |
| lastBatchDate | LocalDateTime | 最后批次时间 |
| lastSyncDate | LocalDateTime | 最后同步时间 |
| totalRows | Integer | 总记录数 |
| syncStatus | Boolean | 同步状态 |

View File

@ -25,9 +25,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="isIncremental" column="is_incremental" />
<result property="objectIndex" column="object_index" />
<result property="batchField" column="batch_field" />
<result property="isPartitioned" column="is_partitioned" />
<result property="totalRows" column="total_rows" />
<result property="lastSyncDate" column="last_sync_date" />
<result property="lastFullSyncDate" column="last_full_sync_date" />
<result property="lastBatchDate" column="last_batch_date" />
<result property="syncStatus" column="sync_status" />
<result property="errorMessage" column="error_message" />
<result property="remark" column="remark" />
@ -59,9 +60,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
dio.is_incremental,
dio.object_index,
dio.batch_field,
dio.is_partitioned,
dio.total_rows,
dio.last_sync_date,
dio.last_full_sync_date,
dio.last_batch_date,
dio.sync_status,
dio.error_message,
dio.remark,
@ -94,9 +96,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="isIncremental != null "> and dio.is_incremental = #{isIncremental}</if>
<if test="objectIndex != null "> and dio.object_index = #{objectIndex}</if>
<if test="batchField != null and batchField != ''"> and dio.batch_field = #{batchField}</if>
<if test="isPartitioned != null "> and dio.is_partitioned = #{isPartitioned}</if>
<if test="totalRows != null "> and dio.total_rows = #{totalRows}</if>
<if test="lastSyncDate != null "> and dio.last_sync_date = #{lastSyncDate}</if>
<if test="lastFullSyncDate != null "> and dio.last_full_sync_date = #{lastFullSyncDate}</if>
<if test="lastBatchDate != null "> and dio.last_batch_date = #{lastBatchDate}</if>
<if test="syncStatus != null "> and dio.sync_status = #{syncStatus}</if>
<if test="errorMessage != null and errorMessage != ''"> and dio.error_message = #{errorMessage}</if>
</where>
@ -134,9 +137,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="isIncremental != null">is_incremental,</if>
<if test="objectIndex != null">object_index,</if>
<if test="batchField != null">batch_field,</if>
<if test="isPartitioned != null">is_partitioned,</if>
<if test="totalRows != null">total_rows,</if>
<if test="lastSyncDate != null">last_sync_date,</if>
<if test="lastFullSyncDate != null">last_full_sync_date,</if>
<if test="lastBatchDate != null">last_batch_date,</if>
<if test="syncStatus != null">sync_status,</if>
<if test="errorMessage != null">error_message,</if>
<if test="remark != null">remark,</if>
@ -165,9 +169,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="isIncremental != null">#{isIncremental},</if>
<if test="objectIndex != null">#{objectIndex},</if>
<if test="batchField != null">#{batchField},</if>
<if test="isPartitioned != null">#{isPartitioned},</if>
<if test="totalRows != null">#{totalRows},</if>
<if test="lastSyncDate != null">#{lastSyncDate},</if>
<if test="lastFullSyncDate != null">#{lastFullSyncDate},</if>
<if test="lastBatchDate != null">#{lastBatchDate},</if>
<if test="syncStatus != null">#{syncStatus},</if>
<if test="errorMessage != null">#{errorMessage},</if>
<if test="remark != null">#{remark},</if>
@ -200,9 +205,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="isIncremental != null">is_incremental = #{isIncremental},</if>
<if test="objectIndex != null">object_index = #{objectIndex},</if>
<if test="batchField != null">batch_field = #{batchField},</if>
<if test="isPartitioned != null">is_partitioned = #{isPartitioned},</if>
<if test="totalRows != null">total_rows = #{totalRows},</if>
<if test="lastSyncDate != null">last_sync_date = #{lastSyncDate},</if>
<if test="lastFullSyncDate != null">last_full_sync_date = #{lastFullSyncDate},</if>
<if test="lastBatchDate != null">last_batch_date = #{lastBatchDate},</if>
<if test="syncStatus != null">sync_status = #{syncStatus},</if>
<if test="errorMessage != null">error_message = #{errorMessage},</if>
<if test="remark != null">remark = #{remark},</if>

View File

@ -0,0 +1,7 @@
-- 为 datai_integration_object 表添加 is_partitioned 字段
-- 执行日期: 2026-01-06
-- 说明: 添加是否分区字段,用于标识对象是否使用分区表
ALTER TABLE `datai_integration_object`
ADD COLUMN `is_partitioned` tinyint NULL DEFAULT 0 COMMENT '是否分区'
AFTER `batch_field`;

View File

@ -346,9 +346,10 @@ CREATE TABLE `datai_integration_object` (
`is_incremental` tinyint NULL DEFAULT 1 COMMENT '增量更新',
`object_index` int NULL DEFAULT 0 COMMENT '排序权重',
`batch_field` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'LastModifiedDate' COMMENT '批次字段',
`is_partitioned` tinyint NULL DEFAULT 0 COMMENT '是否分区',
`total_rows` int NULL DEFAULT 0 COMMENT '本地记录数',
`last_sync_date` datetime NULL DEFAULT NULL COMMENT '最近同步时间',
`last_full_sync_date` datetime NULL DEFAULT NULL COMMENT '全量同步时间',
`last_batch_date` datetime NULL DEFAULT NULL COMMENT '最后批次时间',
`sync_status` tinyint NULL DEFAULT 0 COMMENT '状态',
`error_message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '失败原因',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',