【feat】 提交元数据变更拉取任务

This commit is contained in:
Kris 2026-01-03 17:14:03 +08:00
parent da9ba42769
commit 547b2c6436
27 changed files with 693 additions and 87 deletions

View File

@ -89,6 +89,19 @@ public class BaseController
return rspData;
}
/**
* 响应请求分页数据
*/
protected TableDataInfo getDataTableByPage(List<?> list,long total)
{
TableDataInfo rspData = new TableDataInfo();
rspData.setCode(HttpStatus.SUCCESS);
rspData.setMsg("查询成功");
rspData.setRows(list);
rspData.setTotal(total);
return rspData;
}
/**
* 返回成功
*/

View File

@ -55,7 +55,7 @@ public class ${ClassName}Controller extends BaseController
startPage();
List<${ClassName}> list = ${className}Service.select${ClassName}List(${ClassName}Dto.toObj(${className}Dto));
List<${ClassName}Vo> voList = list.stream().map(${ClassName}Vo::objToVo).collect(Collectors.toList());
return getDataTable(voList);
return getDataTable(voList,PageUtils.getTotal(list));
}
#elseif($table.tree)
public AjaxResult list(${ClassName}Dto ${className}Dto)

View File

@ -4,6 +4,7 @@ import java.util.List;
import java.util.stream.Collectors;
import com.datai.auth.model.vo.DataiSfLoginHistoryVo;
import com.datai.common.utils.PageUtils;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
@ -52,7 +53,7 @@ public class DataiSfLoginHistoryController extends BaseController
startPage();
List<DataiSfLoginHistory> list = dataiSfLoginHistoryService.selectDataiSfLoginHistoryList(DataiSfLoginHistoryDto.toObj(dataiSfLoginHistoryDto));
List<DataiSfLoginHistoryVo> voList = list.stream().map(DataiSfLoginHistoryVo::objToVo).collect(Collectors.toList());
return getDataTable(voList);
return getDataTableByPage(voList,PageUtils.getTotal(list));
}
/**

View File

@ -4,6 +4,7 @@ import java.util.List;
import java.util.stream.Collectors;
import com.datai.auth.model.vo.DataiSfLoginSessionVo;
import com.datai.common.utils.PageUtils;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
@ -52,7 +53,7 @@ public class DataiSfLoginSessionController extends BaseController
startPage();
List<DataiSfLoginSession> list = dataiSfLoginSessionService.selectDataiSfLoginSessionList(DataiSfLoginSessionDto.toObj(dataiSfLoginSessionDto));
List<DataiSfLoginSessionVo> voList = list.stream().map(DataiSfLoginSessionVo::objToVo).collect(Collectors.toList());
return getDataTable(voList);
return getDataTableByPage(voList,PageUtils.getTotal(list));
}
/**

View File

@ -20,7 +20,8 @@ public class SalesforceLoginResult implements Serializable {
private String refreshToken; // 仅在 OAuth 流程中存在
private String tokenType = "Bearer";
private long expiresIn; // 过期时间()
private long loginTimestamp = System.currentTimeMillis(); // 记录登录时间点
private long loginTimestamp; // 记录登录时间点
private long expirationTimestamp; // 记录过期时间点登录时间 + 过期时长
/** 环境信息 */
private String instanceUrl; // API 访问基础地址
@ -49,9 +50,10 @@ public class SalesforceLoginResult implements Serializable {
* 提前 5 分钟判断为过期以预留网络传输时间
*/
public boolean isSessionExpired() {
if (expiresIn <= 0) return false; // 如果永不过期
long bufferMillis = 300 * 1000; // 5分钟缓冲
return System.currentTimeMillis() > (loginTimestamp + (expiresIn * 1000) - bufferMillis);
if (expiresIn <= 0) return false;
if (expirationTimestamp <= 0) return false;
long bufferMillis = 300 * 1000;
return System.currentTimeMillis() > (expirationTimestamp - bufferMillis);
}
// --- 增强的 Getter Setter ---
@ -110,6 +112,12 @@ public class SalesforceLoginResult implements Serializable {
public String getTimeZone() { return timeZone; }
public void setTimeZone(String timeZone) { this.timeZone = timeZone; }
public long getLoginTimestamp() { return loginTimestamp; }
public void setLoginTimestamp(long loginTimestamp) { this.loginTimestamp = loginTimestamp; }
public long getExpirationTimestamp() { return expirationTimestamp; }
public void setExpirationTimestamp(long expirationTimestamp) { this.expirationTimestamp = expirationTimestamp; }
@Override
public String toString() {
return "SalesforceLoginResult{" +

View File

@ -550,6 +550,12 @@ public class LegacyCredentialLoginStrategy implements LoginStrategy {
// 4. 后置处理设置默认过期时间和 Token 类型
fillDefaultExpiration(result);
// 5. 设置登录时间戳和过期时间点
long currentTimestamp = System.currentTimeMillis();
result.setLoginTimestamp(currentTimestamp);
result.setExpirationTimestamp(currentTimestamp + (result.getExpiresIn() * 1000));
return result;
}
@ -617,7 +623,7 @@ public class LegacyCredentialLoginStrategy implements LoginStrategy {
result.setTokenType("Bearer");
}
long expiresIn = 7200L; // 默认值
long expiresIn = 6900L; // 默认值
if (result.getExpiresIn() == 0) {
try {
@ -627,7 +633,7 @@ public class LegacyCredentialLoginStrategy implements LoginStrategy {
.map(c -> CacheUtils.get(salesforceConfigCacheManager.getEnvironmentCacheKey(), "salesforce.session.timeout", String.class))
.filter(StringUtils::isNumeric) // 确保是数字
.map(Long::parseLong)
.orElse(7200L);
.orElse(6900L);
} catch (Exception e) {
logger.warn("读取Salesforce会话超时配置失败使用默认值: 7200", e);
}

View File

@ -340,6 +340,11 @@ public class OAuth2LoginStrategy implements LoginStrategy {
result.setRefreshToken(jsonResponse.get("refresh_token").asText());
}
// 设置登录时间戳和过期时间点
long currentTimestamp = System.currentTimeMillis();
result.setLoginTimestamp(currentTimestamp);
result.setExpirationTimestamp(currentTimestamp + (result.getExpiresIn() * 1000));
return result;
}
@ -377,6 +382,11 @@ public class OAuth2LoginStrategy implements LoginStrategy {
result.setOrganizationId(jsonResponse.get("organization_id").asText());
}
// 设置登录时间戳和过期时间点
long currentTimestamp = System.currentTimeMillis();
result.setLoginTimestamp(currentTimestamp);
result.setExpirationTimestamp(currentTimestamp + (result.getExpiresIn() * 1000));
return result;
}
@ -461,6 +471,11 @@ public class OAuth2LoginStrategy implements LoginStrategy {
STATE_CODE_VERIFIER_MAP.remove(state);
STATE_TIMESTAMP_MAP.remove(state);
// 设置登录时间戳和过期时间点
long currentTimestamp = System.currentTimeMillis();
result.setLoginTimestamp(currentTimestamp);
result.setExpirationTimestamp(currentTimestamp + (result.getExpiresIn() * 1000));
return result;
}

View File

@ -56,6 +56,11 @@ public class SalesforceCliLoginStrategy implements LoginStrategy {
// 3. 解析CLI输出获取Session信息
result = parseCliOutput(output);
result.setSuccess(true);
// 4. 设置登录时间戳和过期时间点
long currentTimestamp = System.currentTimeMillis();
result.setLoginTimestamp(currentTimestamp);
result.setExpirationTimestamp(currentTimestamp + (result.getExpiresIn() * 1000));
} catch (Exception e) {
logger.error("Salesforce CLI登录失败: {}", e.getMessage(), e);
result.setSuccess(false);

View File

@ -80,6 +80,11 @@ public class SessionIdLoginStrategy implements LoginStrategy {
result.setTokenType("Bearer");
result.setExpiresIn(getSessionTimeout(config));
// 设置登录时间戳和过期时间点
long currentTimestamp = System.currentTimeMillis();
result.setLoginTimestamp(currentTimestamp);
result.setExpirationTimestamp(currentTimestamp + (result.getExpiresIn() * 1000));
logger.info("Session ID登录成功用户ID: {}", userInfo.get("user_id"));
} catch (Exception e) {

View File

@ -4,6 +4,7 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.datai.common.utils.PageUtils;
import com.datai.integration.model.vo.DataiIntegrationApiCallLogVo;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
@ -56,7 +57,7 @@ public class DataiIntegrationApiCallLogController extends BaseController
List<DataiIntegrationApiCallLogVo> voList = list.stream()
.map(DataiIntegrationApiCallLogVo::objToVo)
.collect(Collectors.toList());
return getDataTable(voList);
return getDataTableByPage(voList,PageUtils.getTotal(list));
}
/**

View File

@ -4,6 +4,7 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.datai.common.utils.PageUtils;
import com.datai.integration.model.vo.DataiIntegrationBatchVo;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
@ -56,7 +57,7 @@ public class DataiIntegrationBatchController extends BaseController
List<DataiIntegrationBatchVo> voList = list.stream()
.map(DataiIntegrationBatchVo::objToVo)
.collect(Collectors.toList());
return getDataTable(voList);
return getDataTableByPage(voList,PageUtils.getTotal(list));
}
/**

View File

@ -5,6 +5,7 @@ import java.util.Map;
import java.util.HashMap;
import java.util.stream.Collectors;
import com.datai.common.utils.PageUtils;
import com.datai.integration.model.vo.DataiIntegrationBatchHistoryVo;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
@ -57,7 +58,7 @@ public class DataiIntegrationBatchHistoryController extends BaseController
List<DataiIntegrationBatchHistoryVo> voList = list.stream()
.map(DataiIntegrationBatchHistoryVo::objToVo)
.collect(Collectors.toList());
return getDataTable(voList);
return getDataTableByPage(voList,PageUtils.getTotal(list));
}
/**

View File

@ -3,6 +3,7 @@ package com.datai.integration.controller;
import java.util.List;
import java.util.stream.Collectors;
import com.datai.common.utils.PageUtils;
import com.datai.integration.model.vo.DataiIntegrationFieldVo;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
@ -55,7 +56,7 @@ public class DataiIntegrationFieldController extends BaseController
List<DataiIntegrationFieldVo> voList = list.stream()
.map(DataiIntegrationFieldVo::objToVo)
.collect(Collectors.toList());
return getDataTable(voList);
return getDataTableByPage(voList,PageUtils.getTotal(list));
}
/**

View File

@ -3,6 +3,7 @@ package com.datai.integration.controller;
import java.util.List;
import java.util.stream.Collectors;
import com.datai.common.utils.PageUtils;
import com.datai.integration.model.vo.DataiIntegrationFilterLookupVo;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
@ -53,7 +54,7 @@ public class DataiIntegrationFilterLookupController extends BaseController
DataiIntegrationFilterLookup dataiIntegrationFilterLookup = DataiIntegrationFilterLookupDto.toObj(dataiIntegrationFilterLookupDto);
List<DataiIntegrationFilterLookup> list = dataiIntegrationFilterLookupService.selectDataiIntegrationFilterLookupList(dataiIntegrationFilterLookup);
List<DataiIntegrationFilterLookupVo> voList = list.stream().map(DataiIntegrationFilterLookupVo::objToVo).collect(Collectors.toList());
return getDataTable(voList);
return getDataTableByPage(voList,PageUtils.getTotal(list));
}
/**

View File

@ -3,6 +3,7 @@ package com.datai.integration.controller;
import java.util.*;
import java.util.stream.Collectors;
import com.datai.common.utils.PageUtils;
import com.datai.integration.model.domain.DataiIntegrationMetadataChange;
import com.datai.integration.model.vo.DataiIntegrationMetadataChangeVo;
import jakarta.servlet.http.HttpServletResponse;
@ -55,7 +56,7 @@ public class DataiIntegrationMetadataChangeController extends BaseController
DataiIntegrationMetadataChange dataiIntegrationMetadataChange = DataiIntegrationMetadataChangeDto.toObj(dataiIntegrationMetadataChangeDto);
List<DataiIntegrationMetadataChange> list = dataiIntegrationMetadataChangeService.selectDataiIntegrationMetadataChangeList(dataiIntegrationMetadataChange);
List<DataiIntegrationMetadataChangeVo> voList = list.stream().map(DataiIntegrationMetadataChangeVo::objToVo).collect(Collectors.toList());
return getDataTable(voList);
return getDataTableByPage(voList,PageUtils.getTotal(list));
}
/**
@ -70,7 +71,7 @@ public class DataiIntegrationMetadataChangeController extends BaseController
DataiIntegrationMetadataChange dataiIntegrationMetadataChange = DataiIntegrationMetadataChangeDto.toObj(dataiIntegrationMetadataChangeDto);
List<DataiIntegrationMetadataChange> list = dataiIntegrationMetadataChangeService.selectUnsyncedMetadataChangeList(dataiIntegrationMetadataChange);
List<DataiIntegrationMetadataChangeVo> voList = list.stream().map(DataiIntegrationMetadataChangeVo::objToVo).collect(Collectors.toList());
return getDataTable(voList);
return getDataTableByPage(voList,PageUtils.getTotal(list));
}
/**
@ -233,12 +234,21 @@ public class DataiIntegrationMetadataChangeController extends BaseController
/**
* 全对象元数据变更拉取
* 从Salesforce拉取所有对象的元数据变更信息并记录到元数据变更表中
* 该方法会
* 1. 连接到Salesforce获取所有对象的元数据
* 2. 比较现有数据库中的对象元数据
* 3. 记录对象级别的变更新增修改删除
* 4. 记录字段级别的变更新增修改删除
* 5. 检测并记录已从Salesforce中删除的对象
*
* 表的变更新增需要满足以下任一条件
* - isQueryable (可查询)
* - isCreateable (可创建)
* - isUpdateable (可更新)
* - isDeletable (可删除)
* 字段的变更新增无限制
*
* @return 包含拉取结果的AjaxResult包含对象变更数量和字段变更数量
*/
@Operation(summary = "全对象元数据变更拉取")
@PreAuthorize("@ss.hasPermi('integration:change:pullAll')")
@ -247,9 +257,11 @@ public class DataiIntegrationMetadataChangeController extends BaseController
public AjaxResult pullAllMetadataChanges()
{
try {
// 调用服务层方法执行全对象元数据变更拉取
Map<String, Object> result = dataiIntegrationMetadataChangeService.pullAllMetadataChanges();
return success(result);
} catch (Exception e) {
// 记录拉取过程中发生的异常
log.error("全对象元数据变更拉取时发生异常", e);
return error("全对象元数据变更拉取失败: " + e.getMessage());
}

View File

@ -4,6 +4,7 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.datai.common.utils.PageUtils;
import com.datai.integration.model.vo.DataiIntegrationObjectVo;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
@ -54,7 +55,7 @@ public class DataiIntegrationObjectController extends BaseController
DataiIntegrationObject dataiIntegrationObject = DataiIntegrationObjectDto.toObj(dataiIntegrationObjectDto);
List<DataiIntegrationObject> list = dataiIntegrationObjectService.selectDataiIntegrationObjectList(dataiIntegrationObject);
List<DataiIntegrationObjectVo> voList = list.stream().map(DataiIntegrationObjectVo::objToVo).collect(Collectors.toList());
return getDataTable(voList);
return getDataTableByPage(voList,PageUtils.getTotal(list));
}
/**

View File

@ -3,6 +3,7 @@ package com.datai.integration.controller;
import java.util.List;
import java.util.stream.Collectors;
import com.datai.common.utils.PageUtils;
import com.datai.integration.model.vo.DataiIntegrationPicklistVo;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
@ -53,7 +54,7 @@ public class DataiIntegrationPicklistController extends BaseController
DataiIntegrationPicklist dataiIntegrationPicklist = DataiIntegrationPicklistDto.toObj(dataiIntegrationPicklistDto);
List<DataiIntegrationPicklist> list = dataiIntegrationPicklistService.selectDataiIntegrationPicklistList(dataiIntegrationPicklist);
List<DataiIntegrationPicklistVo> voList = list.stream().map(DataiIntegrationPicklistVo::objToVo).collect(Collectors.toList());
return getDataTable(voList);
return getDataTableByPage(voList,PageUtils.getTotal(list));
}
/**

View File

@ -3,6 +3,7 @@ package com.datai.integration.controller;
import java.util.List;
import java.util.stream.Collectors;
import com.datai.common.utils.PageUtils;
import com.datai.integration.model.vo.DataiIntegrationRateLimitVo;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
@ -53,7 +54,7 @@ public class DataiIntegrationRateLimitController extends BaseController
DataiIntegrationRateLimit dataiIntegrationRateLimit = DataiIntegrationRateLimitDto.toObj(dataiIntegrationRateLimitDto);
List<DataiIntegrationRateLimit> list = dataiIntegrationRateLimitService.selectDataiIntegrationRateLimitList(dataiIntegrationRateLimit);
List<DataiIntegrationRateLimitVo> voList = list.stream().map(DataiIntegrationRateLimitVo::objToVo).collect(Collectors.toList());
return getDataTable(voList);
return getDataTableByPage(voList,PageUtils.getTotal(list));
}
/**

View File

@ -1,5 +1,8 @@
package com.datai.integration.core;
import com.datai.auth.model.domain.DataiSfLoginHistory;
import com.datai.auth.service.IDataiSfLoginHistoryService;
import com.datai.common.core.domain.AjaxResult;
import com.datai.salesforce.common.constant.SalesforceConfigConstants;
import com.datai.auth.model.domain.SalesforceLoginResult;
import com.datai.auth.service.ISalesforceLoginService;
@ -21,6 +24,9 @@ public class SessionManager {
@Autowired
private ISalesforceLoginService salesforceLoginService;
@Autowired
private IDataiSfLoginHistoryService dataiSfLoginHistoryService;
/**
* 获取当前Salesforce会话信息
* 如果会话无效则自动尝试重新登录
@ -34,20 +40,13 @@ public class SessionManager {
try {
SalesforceLoginResult loginResult = CacheUtils.get(SalesforceConfigConstants.CACHE_NAME, SalesforceConfigConstants.CURRENT_RESULT, SalesforceLoginResult.class);
if (loginResult != null && loginResult.isSuccess() && !loginResult.isSessionExpired()) {
if (isLoginResultValid(loginResult)) {
log.info("获取会话信息成功,访问令牌前缀: {}",
loginResult.getSessionId() != null ? loginResult.getSessionId().substring(0, Math.min(10, loginResult.getSessionId().length())) : "null");
return loginResult;
}
if (loginResult == null) {
log.warn("会话信息为null尝试自动重新登录");
} else if (!loginResult.isSuccess()) {
log.warn("登录状态为失败,尝试自动重新登录,错误信息: {}", loginResult.getErrorMessage());
} else if (loginResult.isSessionExpired()) {
log.warn("Session已过期尝试自动重新登录");
}
logSessionStatus(loginResult);
return autoLogin();
} catch (Exception e) {
@ -66,7 +65,15 @@ public class SessionManager {
log.info("开始执行自动重新登录");
try {
SalesforceLoginResult result = salesforceLoginService.autoLogin(null);
DataiSfLoginHistory latestLoginHistory = dataiSfLoginHistoryService.selectLatestSuccessLoginHistory();
if (latestLoginHistory == null) {
throw new RuntimeException("未找到登录历史记录" );
}
Long historyId = latestLoginHistory.getId();
SalesforceLoginResult result = salesforceLoginService.autoLogin(historyId);
if (result == null || !result.isSuccess()) {
String errorMsg = result != null ? result.getErrorMessage() : "登录结果为空";
@ -126,36 +133,39 @@ public class SessionManager {
log.info("检查当前Salesforce会话信息是否有效");
try {
SalesforceLoginResult loginResult = CacheUtils.get(SalesforceConfigConstants.CACHE_NAME, SalesforceConfigConstants.CURRENT_RESULT, SalesforceLoginResult.class);
if (loginResult == null) {
log.warn("会话信息为null会话无效");
return false;
}
if (!loginResult.isSuccess()) {
log.warn("登录状态为失败,会话无效");
return false;
}
if (loginResult.getSessionId() == null || loginResult.getSessionId().trim().isEmpty()) {
log.warn("Session ID为空会话无效");
return false;
}
if (loginResult.isSessionExpired()) {
log.warn("Session已过期会话无效");
return false;
}
log.info("会话信息有效Session ID前缀: {}",
loginResult.getSessionId() != null ?
loginResult.getSessionId().substring(0, Math.min(10, loginResult.getSessionId().length())) : "null");
return true;
SalesforceLoginResult loginResult = getCurrentLoginResult();
return isLoginResultValid(loginResult);
} catch (Exception e) {
log.error("检查会话信息有效性时发生异常: {}", e.getMessage(), e);
return false;
}
}
/**
* 判断登录结果是否有效
*
* @param loginResult 登录结果
* @return 如果有效返回true否则返回false
*/
private boolean isLoginResultValid(SalesforceLoginResult loginResult) {
return loginResult != null
&& loginResult.isSuccess()
&& !loginResult.isSessionExpired();
}
/**
* 记录会话状态日志
*
* @param loginResult 登录结果
*/
private void logSessionStatus(SalesforceLoginResult loginResult) {
if (loginResult == null) {
log.warn("会话信息为null尝试自动重新登录");
} else if (!loginResult.isSuccess()) {
log.warn("登录状态为失败,尝试自动重新登录,错误信息: {}", loginResult.getErrorMessage());
} else if (loginResult.isSessionExpired()) {
log.warn("Session已过期尝试自动重新登录");
}
}
}

View File

@ -30,10 +30,18 @@ public abstract class AbstractConnectionFactory<T> implements ISalesforceConnect
String configKey = getConfigKey();
T connection = connectionCache.get(configKey);
if (connection != null && !sessionManager.isSessionValid()) {
log.warn("检测到Session已过期清除缓存的{}连接", getConnectionType());
connectionCache.remove(configKey);
connection = null;
}
if (connection == null) {
lock.lock();
try {
connection = connectionCache.get(configKey);
if (connection == null) {
connection = createConnection();
connectionCache.put(configKey, connection);

View File

@ -692,17 +692,21 @@ public class DataiIntegrationMetadataChangeServiceImpl implements IDataiIntegrat
@Override
public Map<String, Object> pullAllMetadataChanges() {
// 初始化返回结果
Map<String, Object> result = new HashMap<>();
try {
log.info("开始执行全对象元数据变更拉取");
// 建立与Salesforce的连接使用重试机制确保连接成功
PartnerConnection connection = retryOperation(() -> soapConnectionFactory.getConnection(), 3, 1000);
log.info("成功获取Salesforce SOAP连接");
// 获取Salesforce中所有对象的全局描述信息
DescribeGlobalResult globalDescribe = connection.describeGlobal();
DescribeGlobalSObjectResult[] sObjects = globalDescribe.getSobjects();
// 检查是否获取到任何对象
if (sObjects == null || sObjects.length == 0) {
log.warn("未获取到任何Salesforce对象");
result.put("success", true);
@ -714,68 +718,93 @@ public class DataiIntegrationMetadataChangeServiceImpl implements IDataiIntegrat
log.info("从Salesforce获取到 {} 个对象", sObjects.length);
// 用于存储已同步的对象API名称以便后续检测删除的对象
Set<String> syncedObjectApis = new HashSet<>();
int objectChangeCount = 0;
int fieldChangeCount = 0;
int objectChangeCount = 0; // 记录对象变更数量
int fieldChangeCount = 0; // 记录字段变更数量
// 遍历所有Salesforce对象
for (DescribeGlobalSObjectResult sObject : sObjects) {
try {
String objectApi = sObject.getName();
// 检查对象是否需要同步满足查询创建更新或删除任一条件
if (shouldSyncObject(sObject)) {
// 获取对象的详细描述信息
DescribeSObjectResult objDetail = connection.describeSObject(objectApi);
// 查询数据库中是否已存在该对象
DataiIntegrationObject queryObject = new DataiIntegrationObject();
queryObject.setApi(objectApi);
List<DataiIntegrationObject> existingObjects = dataiIntegrationObjectService.selectDataiIntegrationObjectList(queryObject);
// 构建当前对象的元数据信息
DataiIntegrationObject newObject = buildObjectMetadata(objDetail);
boolean hasFieldChange = false;
boolean hasFieldChange = false; // 标记该对象是否有字段变更
// 判断对象是否为新增
if (existingObjects.isEmpty()) {
// 新增对象到数据库
dataiIntegrationObjectService.insertDataiIntegrationObject(newObject);
// 记录对象新增变更
recordObjectChange(newObject, null, "INSERT");
objectChangeCount++;
log.info("新增对象并记录变更: {}", objectApi);
// 重新查询以获取新插入对象的ID
existingObjects = dataiIntegrationObjectService.selectDataiIntegrationObjectList(queryObject);
if (!existingObjects.isEmpty()) {
newObject.setId(existingObjects.get(0).getId());
}
} else {
DataiIntegrationObject existingObject = existingObjects.get(0);
newObject.setId(existingObject.getId());
List<String> changedFields = compareObjects(existingObject, newObject);
if (!changedFields.isEmpty()) {
recordObjectChange(newObject, existingObject, "UPDATE");
objectChangeCount++;
log.debug("记录对象更新: {} - 变更: {}", objectApi, String.join(", ", changedFields));
}
// 将对象API添加到已同步集合中
syncedObjectApis.add(objectApi);
// 对象新增后直接跳过字段比较
continue;
}
// 对象已存在比较新旧对象的差异
DataiIntegrationObject existingObject = existingObjects.get(0);
newObject.setId(existingObject.getId());
List<String> changedFields = compareObjects(existingObject, newObject);
if (!changedFields.isEmpty()) {
// 记录对象更新变更
recordObjectChange(newObject, existingObject, "UPDATE");
objectChangeCount++;
log.debug("记录对象更新: {} - 变更: {}", objectApi, String.join(", ", changedFields));
}
// 将对象API添加到已同步集合中
syncedObjectApis.add(objectApi);
// 查询数据库中该对象的现有字段
DataiIntegrationField queryField = new DataiIntegrationField();
queryField.setApi(objectApi);
List<DataiIntegrationField> existingFields = dataiIntegrationFieldService.selectDataiIntegrationFieldList(queryField);
// 将现有字段转换为Map便于快速查找
Map<String, DataiIntegrationField> existingFieldMap = new HashMap<>();
for (DataiIntegrationField existingField : existingFields) {
existingFieldMap.put(existingField.getField(), existingField);
}
// 遍历Salesforce中的所有字段
for (Field field : objDetail.getFields()) {
// 构建当前字段的元数据信息
DataiIntegrationField newField = buildFieldMetadata(objectApi, field);
// 检查字段是否已存在
DataiIntegrationField existingField = existingFieldMap.get(field.getName());
if (existingField == null) {
// 字段为新增
recordFieldChange(objectApi, newObject.getLabel(), newField.getField(),
newField.getLabel(), null, "INSERT", "新增字段", newObject.getIsCustom());
fieldChangeCount++;
hasFieldChange = true;
log.debug("记录字段新增: {}.{}", objectApi, field.getName());
} else {
// 字段已存在比较新旧字段的差异
List<String> changedFieldProps = compareFields(existingField, newField);
if (!changedFieldProps.isEmpty()) {
// 记录字段更新变更
recordFieldChange(objectApi, newObject.getLabel(), newField.getField(),
newField.getLabel(), "字段属性变更: " + String.join(", ", changedFieldProps),
"UPDATE", "字段属性更新", newObject.getIsCustom());
@ -787,6 +816,7 @@ public class DataiIntegrationMetadataChangeServiceImpl implements IDataiIntegrat
}
}
// 检查是否有已删除的字段在数据库中存在但在Salesforce中不存在
for (DataiIntegrationField existingField : existingFields) {
boolean fieldExists = false;
for (Field field : objDetail.getFields()) {
@ -796,6 +826,7 @@ public class DataiIntegrationMetadataChangeServiceImpl implements IDataiIntegrat
}
}
if (!fieldExists) {
// 记录字段删除变更
recordFieldChange(objectApi, newObject.getLabel(), existingField.getField(),
existingField.getLabel(), "字段已从Salesforce中删除",
"DELETE", "字段删除", newObject.getIsCustom());
@ -805,23 +836,27 @@ public class DataiIntegrationMetadataChangeServiceImpl implements IDataiIntegrat
}
}
// 如果该对象有字段变更禁用其增量更新状态
if (hasFieldChange && newObject.getId() != null) {
DataiIntegrationObject updateObject = new DataiIntegrationObject();
updateObject.setId(newObject.getId());
updateObject.setIsIncremental(false);
updateObject.setIsIncremental(false); // 禁用增量更新
dataiIntegrationObjectService.updateDataiIntegrationObject(updateObject);
log.info("检测到字段变更,已禁用对象 {} 的增量更新状态", objectApi);
}
}
} catch (Exception e) {
// 记录处理单个对象时的错误但继续处理其他对象
log.error("处理对象 {} 时出错: {}", sObject.getName(), e.getMessage(), e);
}
}
// 检查并记录已从Salesforce中删除的对象
checkDeletedObjectsForMetadata(syncedObjectApis);
log.info("全对象元数据变更拉取完成,对象变更: {} 个,字段变更: {} 个", objectChangeCount, fieldChangeCount);
// 设置返回结果
result.put("success", true);
result.put("objectChangeCount", objectChangeCount);
result.put("fieldChangeCount", fieldChangeCount);
@ -829,6 +864,7 @@ public class DataiIntegrationMetadataChangeServiceImpl implements IDataiIntegrat
return result;
} catch (Exception e) {
// 记录整个拉取过程中的异常
log.error("全对象元数据变更拉取时发生异常", e);
result.put("success", false);
result.put("message", "全对象元数据变更拉取失败: " + e.getMessage());
@ -836,114 +872,218 @@ public class DataiIntegrationMetadataChangeServiceImpl implements IDataiIntegrat
}
}
/**
* 判断对象是否需要同步
* 只有满足以下任一条件的对象才需要同步
* - 可查询 (isQueryable)
* - 可创建 (isCreateable)
* - 可更新 (isUpdateable)
* - 可删除 (isDeletable)
*
* @param sObject Salesforce对象描述信息
* @return 如果对象需要同步返回true否则返回false
*/
private boolean shouldSyncObject(DescribeGlobalSObjectResult sObject) {
return sObject.isQueryable() || sObject.isCreateable() || sObject.isUpdateable() || sObject.isDeletable();
}
/**
* 根据Salesforce对象描述信息构建对象元数据实体
* 将Salesforce的DescribeSObjectResult转换为DataiIntegrationObject实体
*
* @param objDetail Salesforce对象的详细描述信息
* @return 构建的DataiIntegrationObject实体如果构建失败返回null
*/
private DataiIntegrationObject buildObjectMetadata(DescribeSObjectResult objDetail) {
try {
// 创建对象元数据实体
DataiIntegrationObject object = new DataiIntegrationObject();
// 设置对象API名称
object.setApi(objDetail.getName());
// 设置对象标签名称
object.setLabel(objDetail.getLabel());
// 设置对象复数标签名称
object.setLabelPlural(objDetail.getLabelPlural());
// 设置对象键前缀
object.setKeyPrefix(objDetail.getKeyPrefix());
// 设置是否为自定义对象
object.setIsCustom(objDetail.isCustom());
// 设置是否为自定义设置对象
object.setIsCustomSetting(objDetail.isCustomSetting());
// 设置是否可查询
object.setIsQueryable(objDetail.isQueryable());
// 设置是否可创建
object.setIsCreateable(objDetail.isCreateable());
// 设置是否可更新
object.setIsUpdateable(objDetail.isUpdateable());
// 设置是否可删除
object.setIsDeletable(objDetail.isDeletable());
// 设置是否可复制
object.setIsReplicateable(objDetail.isReplicateable());
// 设置是否可检索
object.setIsRetrieveable(objDetail.isRetrieveable());
// 设置是否可搜索
object.setIsSearchable(objDetail.isSearchable());
object.setIsWork(true);
object.setIsIncremental(true);
// 设置是否工作状态启用
object.setIsWork(false);
// 设置是否启用增量更新
object.setIsIncremental(false);
// 设置最后同步时间
object.setLastSyncDate(LocalDateTime.now());
return object;
} catch (Exception e) {
// 记录构建对象元数据时的错误
log.error("构建对象 {} 元数据时出错: {}", objDetail.getName(), e.getMessage(), e);
return null;
}
}
/**
* 根据Salesforce字段描述信息构建字段元数据实体
* 将Salesforce的Field对象转换为DataiIntegrationField实体
*
* @param objectApi 所属对象的API名称
* @param field Salesforce字段描述信息
* @return 构建的DataiIntegrationField实体
*/
private DataiIntegrationField buildFieldMetadata(String objectApi, Field field) {
// 创建字段元数据实体
DataiIntegrationField fieldEntity = new DataiIntegrationField();
// 设置所属对象API名称
fieldEntity.setApi(objectApi);
// 设置字段名称
fieldEntity.setField(field.getName());
// 设置字段标签
fieldEntity.setLabel(field.getLabel());
// 设置是否可创建
fieldEntity.setIsCreateable(field.isCreateable());
// 设置是否可为空
fieldEntity.setIsNillable(field.isNillable());
// 设置是否可更新
fieldEntity.setIsUpdateable(field.isUpdateable());
// 设置是否在创建时有默认值
fieldEntity.setIsDefaultedOnCreate(field.isDefaultedOnCreate());
// 设置是否唯一
fieldEntity.setIsUnique(field.isUnique());
// 设置是否可过滤
fieldEntity.setIsFilterable(field.isFilterable());
// 设置是否可排序
fieldEntity.setIsSortable(field.isSortable());
// 设置是否可聚合
fieldEntity.setIsAggregatable(field.isAggregatable());
// 设置是否可分组
fieldEntity.setIsGroupable(field.isGroupable());
// 设置是否为多态外键
fieldEntity.setIsPolymorphicForeignKey(field.isPolymorphicForeignKey());
// 设置多态外键类型字段
fieldEntity.setPolymorphicForeignField(field.getName() + "_type");
// 设置是否为外部ID
fieldEntity.setIsExternalId(field.isExternalId());
// 设置是否为自定义字段
fieldEntity.setIsCustom(field.isCustom());
// 设置是否为计算字段
fieldEntity.setIsCalculated(field.isCalculated());
// 设置是否为自动编号字段
fieldEntity.setIsAutoNumber(field.isAutoNumber());
// 设置是否区分大小写
fieldEntity.setIsCaseSensitive(field.isCaseSensitive());
// 设置是否为加密字段
fieldEntity.setIsEncrypted(field.isEncrypted());
// 设置是否为HTML格式化字段
fieldEntity.setIsHtmlFormatted(field.isHtmlFormatted());
// 设置是否为ID查找字段
fieldEntity.setIsIdLookup(field.isIdLookup());
// 设置是否为权限字段
fieldEntity.setIsPermissionable(field.isPermissionable());
// 设置是否为受限选择列表
fieldEntity.setIsRestrictedPicklist(field.isRestrictedPicklist());
// 设置是否为受限删除
fieldEntity.setIsRestrictedDelete(field.isRestrictedDelete());
// 设置写入是否需要主记录读取权限
fieldEntity.setIsWriteRequiresMasterRead(field.isWriteRequiresMasterRead());
// 设置字段数据类型
fieldEntity.setFieldDataType(field.getType() != null ? field.getType().toString() : null);
// 设置字段长度
fieldEntity.setFieldLength(field.getLength());
// 设置字段精度
fieldEntity.setFieldPrecision(field.getPrecision());
// 设置字段小数位数
fieldEntity.setFieldScale(field.getScale());
// 设置字段字节长度
fieldEntity.setFieldByteLength(field.getByteLength());
// 设置默认值公式
fieldEntity.setDefaultValue(field.getDefaultValueFormula());
// 设置计算公式
fieldEntity.setCalculatedFormula(field.getCalculatedFormula());
// 设置内联帮助文本
fieldEntity.setInlineHelpText(field.getInlineHelpText());
// 设置关系名称
fieldEntity.setRelationshipName(field.getRelationshipName());
// 设置关系顺序
fieldEntity.setRelationshipOrder(field.getRelationshipOrder());
// 设置引用目标字段
fieldEntity.setReferenceTargetField(field.getReferenceTo() != null && field.getReferenceTo().length > 0 ? field.getReferenceTo()[0] : null);
// 如果字段有引用目标则设置引用目标
if (field.getReferenceTo() != null && field.getReferenceTo().length > 0) {
// 设置引用目标多个引用目标用逗号分隔
fieldEntity.setReferenceTo(String.join(",", field.getReferenceTo()));
// 设置第一个引用目标字段
fieldEntity.setReferenceTargetField(field.getReferenceTo()[0]);
}
return fieldEntity;
}
/**
* 比较两个对象元数据的差异
* 检查新旧对象元数据之间的不同属性并返回有变化的属性列表
*
* @param oldObject 旧的对象元数据
* @param newObject 新的对象元数据
* @return 有变化的属性名称列表
*/
private List<String> compareObjects(DataiIntegrationObject oldObject, DataiIntegrationObject newObject) {
// 初始化变更字段列表
List<String> changedFields = new ArrayList<>();
// 比较标签名称
if (!Objects.equals(oldObject.getLabel(), newObject.getLabel())) {
changedFields.add("label");
}
// 比较复数标签名称
if (!Objects.equals(oldObject.getLabelPlural(), newObject.getLabelPlural())) {
changedFields.add("labelPlural");
}
// 比较键前缀
if (!Objects.equals(oldObject.getKeyPrefix(), newObject.getKeyPrefix())) {
changedFields.add("keyPrefix");
}
// 比较是否可查询状态
if (!Objects.equals(oldObject.getIsQueryable(), newObject.getIsQueryable())) {
changedFields.add("isQueryable");
}
// 比较是否可创建状态
if (!Objects.equals(oldObject.getIsCreateable(), newObject.getIsCreateable())) {
changedFields.add("isCreateable");
}
// 比较是否可更新状态
if (!Objects.equals(oldObject.getIsUpdateable(), newObject.getIsUpdateable())) {
changedFields.add("isUpdateable");
}
// 比较是否可删除状态
if (!Objects.equals(oldObject.getIsDeletable(), newObject.getIsDeletable())) {
changedFields.add("isDeletable");
}
// 比较是否可复制状态
if (!Objects.equals(oldObject.getIsReplicateable(), newObject.getIsReplicateable())) {
changedFields.add("isReplicateable");
}
// 比较是否可检索状态
if (!Objects.equals(oldObject.getIsRetrieveable(), newObject.getIsRetrieveable())) {
changedFields.add("isRetrieveable");
}
// 比较是否可搜索状态
if (!Objects.equals(oldObject.getIsSearchable(), newObject.getIsSearchable())) {
changedFields.add("isSearchable");
}
@ -951,105 +1091,147 @@ public class DataiIntegrationMetadataChangeServiceImpl implements IDataiIntegrat
return changedFields;
}
/**
* 比较两个字段元数据的差异
* 检查新旧字段元数据之间的不同属性并返回有变化的属性列表
*
* @param oldField 旧的字段元数据
* @param newField 新的字段元数据
* @return 有变化的属性名称列表
*/
private List<String> compareFields(DataiIntegrationField oldField, DataiIntegrationField newField) {
// 初始化变更字段列表
List<String> changedFields = new ArrayList<>();
// 比较标签名称
if (!Objects.equals(oldField.getLabel(), newField.getLabel())) {
changedFields.add("label");
}
// 比较是否可创建状态
if (!Objects.equals(oldField.getIsCreateable(), newField.getIsCreateable())) {
changedFields.add("isCreateable");
}
// 比较是否可为空状态
if (!Objects.equals(oldField.getIsNillable(), newField.getIsNillable())) {
changedFields.add("isNillable");
}
// 比较是否可更新状态
if (!Objects.equals(oldField.getIsUpdateable(), newField.getIsUpdateable())) {
changedFields.add("isUpdateable");
}
// 比较是否在创建时有默认值状态
if (!Objects.equals(oldField.getIsDefaultedOnCreate(), newField.getIsDefaultedOnCreate())) {
changedFields.add("isDefaultedOnCreate");
}
// 比较是否唯一状态
if (!Objects.equals(oldField.getIsUnique(), newField.getIsUnique())) {
changedFields.add("isUnique");
}
// 比较是否可过滤状态
if (!Objects.equals(oldField.getIsFilterable(), newField.getIsFilterable())) {
changedFields.add("isFilterable");
}
// 比较是否可排序状态
if (!Objects.equals(oldField.getIsSortable(), newField.getIsSortable())) {
changedFields.add("isSortable");
}
// 比较是否可聚合状态
if (!Objects.equals(oldField.getIsAggregatable(), newField.getIsAggregatable())) {
changedFields.add("isAggregatable");
}
// 比较是否可分组状态
if (!Objects.equals(oldField.getIsGroupable(), newField.getIsGroupable())) {
changedFields.add("isGroupable");
}
// 比较是否为多态外键状态
if (!Objects.equals(oldField.getIsPolymorphicForeignKey(), newField.getIsPolymorphicForeignKey())) {
changedFields.add("isPolymorphicForeignKey");
}
// 比较是否为外部ID状态
if (!Objects.equals(oldField.getIsExternalId(), newField.getIsExternalId())) {
changedFields.add("isExternalId");
}
// 比较是否为自定义字段状态
if (!Objects.equals(oldField.getIsCustom(), newField.getIsCustom())) {
changedFields.add("isCustom");
}
// 比较是否为计算字段状态
if (!Objects.equals(oldField.getIsCalculated(), newField.getIsCalculated())) {
changedFields.add("isCalculated");
}
// 比较是否为自动编号字段状态
if (!Objects.equals(oldField.getIsAutoNumber(), newField.getIsAutoNumber())) {
changedFields.add("isAutoNumber");
}
// 比较是否区分大小写状态
if (!Objects.equals(oldField.getIsCaseSensitive(), newField.getIsCaseSensitive())) {
changedFields.add("isCaseSensitive");
}
// 比较是否为加密字段状态
if (!Objects.equals(oldField.getIsEncrypted(), newField.getIsEncrypted())) {
changedFields.add("isEncrypted");
}
// 比较是否为HTML格式化字段状态
if (!Objects.equals(oldField.getIsHtmlFormatted(), newField.getIsHtmlFormatted())) {
changedFields.add("isHtmlFormatted");
}
// 比较是否为ID查找字段状态
if (!Objects.equals(oldField.getIsIdLookup(), newField.getIsIdLookup())) {
changedFields.add("isIdLookup");
}
// 比较是否为权限字段状态
if (!Objects.equals(oldField.getIsPermissionable(), newField.getIsPermissionable())) {
changedFields.add("isPermissionable");
}
// 比较是否为受限选择列表状态
if (!Objects.equals(oldField.getIsRestrictedPicklist(), newField.getIsRestrictedPicklist())) {
changedFields.add("isRestrictedPicklist");
}
// 比较是否为受限删除状态
if (!Objects.equals(oldField.getIsRestrictedDelete(), newField.getIsRestrictedDelete())) {
changedFields.add("isRestrictedDelete");
}
// 比较写入是否需要主记录读取权限状态
if (!Objects.equals(oldField.getIsWriteRequiresMasterRead(), newField.getIsWriteRequiresMasterRead())) {
changedFields.add("isWriteRequiresMasterRead");
}
// 比较字段数据类型
if (!Objects.equals(oldField.getFieldDataType(), newField.getFieldDataType())) {
changedFields.add("fieldDataType");
}
// 比较字段长度
if (!Objects.equals(oldField.getFieldLength(), newField.getFieldLength())) {
changedFields.add("fieldLength");
}
// 比较字段精度
if (!Objects.equals(oldField.getFieldPrecision(), newField.getFieldPrecision())) {
changedFields.add("fieldPrecision");
}
// 比较字段小数位数
if (!Objects.equals(oldField.getFieldScale(), newField.getFieldScale())) {
changedFields.add("fieldScale");
}
// 比较字段字节长度
if (!Objects.equals(oldField.getFieldByteLength(), newField.getFieldByteLength())) {
changedFields.add("fieldByteLength");
}
// 比较默认值公式
if (!Objects.equals(oldField.getDefaultValue(), newField.getDefaultValue())) {
changedFields.add("defaultValue");
}
// 比较计算公式
if (!Objects.equals(oldField.getCalculatedFormula(), newField.getCalculatedFormula())) {
changedFields.add("calculatedFormula");
}
// 比较内联帮助文本
if (!Objects.equals(oldField.getInlineHelpText(), newField.getInlineHelpText())) {
changedFields.add("inlineHelpText");
}
// 比较关系名称
if (!Objects.equals(oldField.getRelationshipName(), newField.getRelationshipName())) {
changedFields.add("relationshipName");
}
// 比较引用目标
if (!Objects.equals(oldField.getReferenceTo(), newField.getReferenceTo())) {
changedFields.add("referenceTo");
}
@ -1057,102 +1239,189 @@ public class DataiIntegrationMetadataChangeServiceImpl implements IDataiIntegrat
return changedFields;
}
/**
* 记录对象元数据变更
* 将对象级别的变更新增修改删除记录到元数据变更表中
*
* @param newObject 新的对象元数据
* @param oldObject 旧的对象元数据新增操作时为null
* @param operationType 操作类型INSERTUPDATEDELETE
*/
private void recordObjectChange(DataiIntegrationObject newObject, DataiIntegrationObject oldObject, String operationType) {
try {
// 创建元数据变更记录实体
DataiIntegrationMetadataChange metadataChange = new DataiIntegrationMetadataChange();
// 设置变更类型为对象
metadataChange.setChangeType("OBJECT");
// 设置操作类型INSERTUPDATEDELETE
metadataChange.setOperationType(operationType);
// 设置对象API名称
metadataChange.setObjectApi(newObject.getApi());
// 设置对象标签名称
metadataChange.setObjectLabel(newObject.getLabel());
// 设置变更时间
metadataChange.setChangeTime(LocalDateTime.now());
// 设置同步状态为未同步
metadataChange.setSyncStatus(false);
// 设置是否为自定义对象
metadataChange.setIsCustom(newObject.getIsCustom());
// 如果是更新操作oldObject不为null则比较新旧对象的差异
if (oldObject != null) {
List<String> changedFields = compareObjects(oldObject, newObject);
if (!changedFields.isEmpty()) {
// 如果有具体变更字段则记录详细变更信息
metadataChange.setChangeReason("对象属性变更: " + String.join(", ", changedFields));
} else {
// 如果没有具体变更字段则记录通用更新信息
metadataChange.setChangeReason("对象属性更新");
}
} else {
// 如果是新增操作则记录为新增对象
metadataChange.setChangeReason("新增对象");
}
// 设置变更用户为系统
metadataChange.setChangeUser("SYSTEM");
// 插入元数据变更记录
insertDataiIntegrationMetadataChange(metadataChange);
log.debug("记录对象变更成功: {} - {}", newObject.getApi(), operationType);
} catch (Exception e) {
// 记录记录对象变更时的错误
log.error("记录对象变更失败: {} - {}", newObject.getApi(), e.getMessage(), e);
}
}
/**
* 记录字段元数据变更
* 将字段级别的变更新增修改删除记录到元数据变更表中
*
* @param objectApi 所属对象的API名称
* @param objectLabel 所属对象的标签名称
* @param fieldApi 字段API名称
* @param fieldLabel 字段标签名称
* @param changeReason 变更原因可为null使用defaultReason
* @param operationType 操作类型INSERTUPDATEDELETE
* @param defaultReason 默认变更原因
* @param isCustom 是否为自定义对象
*/
private void recordFieldChange(String objectApi, String objectLabel, String fieldApi, String fieldLabel,
String changeReason, String operationType, String defaultReason, Boolean isCustom) {
try {
// 创建元数据变更记录实体
DataiIntegrationMetadataChange metadataChange = new DataiIntegrationMetadataChange();
// 设置变更类型为字段
metadataChange.setChangeType("FIELD");
// 设置操作类型INSERTUPDATEDELETE
metadataChange.setOperationType(operationType);
// 设置对象API名称
metadataChange.setObjectApi(objectApi);
// 设置对象标签名称
metadataChange.setObjectLabel(objectLabel);
// 设置字段API名称
metadataChange.setFieldApi(fieldApi);
// 设置字段标签名称
metadataChange.setFieldLabel(fieldLabel);
// 设置变更时间
metadataChange.setChangeTime(LocalDateTime.now());
// 设置同步状态为未同步
metadataChange.setSyncStatus(false);
// 设置是否为自定义对象
metadataChange.setIsCustom(isCustom);
// 设置变更原因如果changeReason不为null则使用它否则使用默认原因
metadataChange.setChangeReason(changeReason != null ? changeReason : defaultReason);
// 设置变更用户为系统
metadataChange.setChangeUser("SYSTEM");
// 插入元数据变更记录
insertDataiIntegrationMetadataChange(metadataChange);
log.debug("记录字段变更成功: {}.{} - {}", objectApi, fieldApi, operationType);
} catch (Exception e) {
// 记录记录字段变更时的错误
log.error("记录字段变更失败: {}.{} - {}", objectApi, fieldApi, e.getMessage(), e);
}
}
/**
* 检查并记录已从Salesforce中删除的对象
* 通过比较数据库中存储的对象与从Salesforce获取的对象列表
* 识别出已从Salesforce中删除的对象并记录相应的删除变更
*
* @param syncedObjectApis 从Salesforce获取到的已同步对象API名称集合
*/
private void checkDeletedObjectsForMetadata(Set<String> syncedObjectApis) {
try {
// 查询数据库中所有对象
DataiIntegrationObject queryObject = new DataiIntegrationObject();
queryObject.setIsWork(true);
List<DataiIntegrationObject> allObjects = dataiIntegrationObjectService.selectDataiIntegrationObjectList(queryObject);
// 遍历数据库中的所有对象检查哪些对象已不在Salesforce中
for (DataiIntegrationObject object : allObjects) {
if (!syncedObjectApis.contains(object.getApi())) {
// 创建元数据变更记录标记为删除操作
DataiIntegrationMetadataChange metadataChange = new DataiIntegrationMetadataChange();
// 设置变更类型为对象
metadataChange.setChangeType("OBJECT");
// 设置操作类型为删除
metadataChange.setOperationType("DELETE");
// 设置对象API名称
metadataChange.setObjectApi(object.getApi());
// 设置对象标签名称
metadataChange.setObjectLabel(object.getLabel());
// 设置变更时间
metadataChange.setChangeTime(LocalDateTime.now());
// 设置同步状态为未同步
metadataChange.setSyncStatus(false);
// 设置是否为自定义对象
metadataChange.setIsCustom(object.getIsCustom());
// 设置变更原因
metadataChange.setChangeReason("对象已从Salesforce中删除");
// 设置变更用户为系统
metadataChange.setChangeUser("SYSTEM");
// 插入元数据变更记录
insertDataiIntegrationMetadataChange(metadataChange);
log.warn("检测到对象已删除: {}", object.getApi());
}
}
} catch (Exception e) {
// 记录检查已删除对象时的错误
log.error("检查已删除对象时出错: {}", e.getMessage(), e);
}
}
/**
* 重试操作执行
* 对指定的操作执行重试机制在操作失败时进行重试直到达到最大重试次数
*
* @param <T> 操作返回值类型
* @param operation 需要执行的操作Supplier函数式接口
* @param maxRetries 最大重试次数
* @param delayMs 每次重试前的延迟时间毫秒
* @return 操作执行结果
*/
private <T> T retryOperation(java.util.function.Supplier<T> operation, int maxRetries, long delayMs) {
// 初始化重试计数器
int retries = 0;
// 无限循环直到操作成功或达到最大重试次数
while (true) {
try {
// 执行操作并返回结果
return operation.get();
} catch (Exception e) {
// 增加重试计数
retries++;
if (retries >= maxRetries) {
// 如果达到最大重试次数记录错误并抛出异常
log.error("操作失败,已达到最大重试次数: {}", maxRetries, e);
throw e;
}
try {
// 记录重试日志并延迟指定时间
log.warn("操作失败,将在 {} 毫秒后重试,当前重试次数: {}", delayMs, retries);
Thread.sleep(delayMs);
} catch (InterruptedException ie) {
// 如果线程在延迟期间被中断恢复中断状态并抛出运行时异常
Thread.currentThread().interrupt();
log.error("重试延迟时线程被中断", ie);
throw new RuntimeException(ie);

View File

@ -1,7 +1,9 @@
package com.datai.integration.task;
import com.alibaba.fastjson2.JSON;
import com.datai.integration.model.domain.DataiIntegrationMetadataChange;
import com.datai.integration.model.param.DataiSyncParam;
import com.datai.integration.service.IDataiIntegrationMetadataChangeService;
import com.datai.salesforce.common.utils.ApiListUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
@ -15,19 +17,9 @@ public class SalesforceSyncTask {
@Autowired
private ISalesforceDataPullService salesforceDataPullService;
public void syncAllData(String paramStr) {
DataiSyncParam dataiSyncParam = new DataiSyncParam();
if (StringUtils.isNotBlank(paramStr)) {
dataiSyncParam = JSON.parseObject(paramStr, DataiSyncParam.class);
}
List<String> strings = ApiListUtils.splitApiList(dataiSyncParam.getApi());
salesforceDataPullService.syncObjectsData(strings);
}
@Autowired
private IDataiIntegrationMetadataChangeService dataiIntegrationMetadataChangeService;
public void syncByObjectStructures(String paramStr) {
@ -41,4 +33,8 @@ public class SalesforceSyncTask {
salesforceDataPullService.syncObjectStructures(strings);
}
public void pullAllMetadataChanges() {
dataiIntegrationMetadataChangeService.pullAllMetadataChanges();
}
}

View File

@ -0,0 +1,245 @@
# 全对象元数据变更拉取接口文档
## 1. 接口概述
### 1.1 接口名称
全对象元数据变更拉取
### 1.2 接口描述
从Salesforce拉取所有对象的元数据变更信息并记录到元数据变更表中。该接口会自动检测对象和字段的新增、修改、删除操作并将变更记录到数据库中。
### 1.3 接口地址
```
POST /integration/change/pullAll
```
### 1.4 请求方式
POST
### 1.5 权限要求
需要 `integration:change:pullAll` 权限
### 1.6 日志记录
操作类型OTHER其他
日志标题:全对象元数据变更拉取
## 2. 请求参数
### 2.1 请求头
```
Content-Type: application/json
Authorization: Bearer {token}
```
### 2.2 请求体
该接口无需请求体参数
### 2.3 请求示例
```bash
curl -X POST "http://localhost:8080/integration/change/pullAll" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {your_token}"
```
## 3. 响应格式
### 3.1 响应结构
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"success": true,
"objectChangeCount": 5,
"fieldChangeCount": 23,
"message": "全对象元数据变更拉取完成,对象变更: 5 个,字段变更: 23 个"
}
}
```
### 3.2 响应字段说明
| 字段名 | 类型 | 说明 |
|--------|------|------|
| code | Integer | 响应状态码200表示成功 |
| msg | String | 响应消息 |
| data | Object | 响应数据对象 |
| data.success | Boolean | 操作是否成功 |
| data.objectChangeCount | Integer | 对象变更数量 |
| data.fieldChangeCount | Integer | 字段变更数量 |
| data.message | String | 详细消息说明 |
### 3.3 成功响应示例
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"success": true,
"objectChangeCount": 3,
"fieldChangeCount": 15,
"message": "全对象元数据变更拉取完成,对象变更: 3 个,字段变更: 15 个"
}
}
```
### 3.4 失败响应示例
```json
{
"code": 500,
"msg": "全对象元数据变更拉取失败: 无法连接到Salesforce",
"data": null
}
```
## 4. 业务逻辑说明
### 4.1 处理流程
1. **建立连接**使用重试机制最多3次每次间隔1秒建立与Salesforce的SOAP连接
2. **获取对象列表**从Salesforce获取所有对象的全局描述信息
3. **对象过滤**:只同步满足以下任一条件的对象:
- isQueryable可查询
- isCreateable可创建
- isUpdateable可更新
- isDeletable可删除
4. **对象变更检测**
- 新增对象:插入数据库并记录变更
- 修改对象:比较差异并记录变更
- 删除对象:检测并记录已删除的对象
5. **字段变更检测**
- 新增字段:记录字段新增变更
- 修改字段:比较字段属性差异并记录变更
- 删除字段:检测并记录已删除的字段
6. **增量更新处理**:如果检测到字段变更,自动禁用该对象的增量更新状态
7. **统计结果**:返回对象变更数量和字段变更数量
### 4.2 对象同步条件
只有满足以下任一条件的对象才会被同步:
- 可查询isQueryable = true
- 可创建isCreateable = true
- 可更新isUpdateable = true
- 可删除isDeletable = true
### 4.3 字段同步条件
所有字段都会被同步,无特殊限制
### 4.4 变更记录规则
- **对象变更**:记录到元数据变更表,变更类型为 "OBJECT"
- **字段变更**:记录到元数据变更表,变更类型为 "FIELD"
- **操作类型**INSERT新增、UPDATE修改、DELETE删除
- **同步状态**:默认为 false未同步
## 5. 错误处理
### 5.1 常见错误码
| 错误码 | 说明 | 处理建议 |
|--------|------|----------|
| 401 | 未授权 | 检查token是否有效 |
| 403 | 权限不足 | 确认用户是否具有 `integration:change:pullAll` 权限 |
| 500 | 服务器内部错误 | 查看服务器日志检查Salesforce连接配置 |
### 5.2 常见错误场景
#### 5.2.1 Salesforce连接失败
```
错误信息:全对象元数据变更拉取失败: 无法连接到Salesforce
原因SOAP连接配置错误或网络问题
解决方案检查SOAPConnectionFactory配置和网络连接
```
#### 5.2.2 权限不足
```
错误信息:您没有权限执行此操作
原因:用户缺少 `integration:change:pullAll` 权限
解决方案:联系管理员分配相应权限
```
#### 5.2.3 未获取到对象
```
错误信息未获取到任何Salesforce对象
原因Salesforce中无可访问对象或API权限不足
解决方案检查Salesforce API权限配置
```
## 6. 使用注意事项
### 6.1 性能考虑
- 该接口会遍历Salesforce中的所有对象和字段可能需要较长时间
- 建议在业务低峰期调用
- 避免频繁调用,建议按需触发
### 6.2 数据一致性
- 接口执行过程中会记录详细的变更日志
- 如果执行失败,已记录的变更不会回滚
- 建议在执行前备份重要数据
### 6.3 并发控制
- 不建议同时调用多个拉取接口
- 确保前一次拉取完成后再执行下一次拉取
### 6.4 监控建议
- 监控接口执行时间
- 监控变更数量,异常增长可能需要人工介入
- 定期检查同步状态,及时处理未同步的变更
## 7. 相关接口
### 7.1 查询元数据变更列表
```
GET /integration/change/list
```
### 7.2 查询未同步的元数据变更
```
GET /integration/change/unsynced
```
### 7.3 同步元数据变更到本地数据库
```
POST /integration/change/{id}/sync
```
### 7.4 批量同步元数据变更
```
POST /integration/change/syncBatch
```
### 7.5 获取变更统计信息
```
GET /integration/change/statistics
```
## 8. 附录
### 8.1 元数据变更表结构
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | Long | 主键ID |
| changeType | String | 变更类型OBJECT/FIELD |
| operationType | String | 操作类型INSERT/UPDATE/DELETE |
| objectApi | String | 对象API名称 |
| objectLabel | String | 对象标签名称 |
| fieldApi | String | 字段API名称字段变更时 |
| fieldLabel | String | 字段标签名称(字段变更时) |
| changeReason | String | 变更原因 |
| changeTime | LocalDateTime | 变更时间 |
| syncStatus | Boolean | 同步状态false-未同步true-已同步) |
| isCustom | Boolean | 是否为自定义对象 |
| changeUser | String | 变更用户 |
### 8.2 版本历史
| 版本 | 日期 | 说明 |
|------|------|------|
| 1.0 | 2025-12-27 | 初始版本 |
### 8.3 联系方式
如有问题,请联系技术支持团队。
---
**文档生成时间**2025-12-27
**最后更新时间**2025-12-27
**文档版本**1.0

View File

@ -3,6 +3,7 @@ package com.datai.setting.controller;
import java.util.List;
import java.util.stream.Collectors;
import com.datai.common.utils.PageUtils;
import com.datai.setting.model.dto.DataiConfigAuditLogDto;
import com.datai.setting.model.vo.DataiConfigAuditLogVo;
import com.datai.setting.utils.AuditLogObjectNameResolver;
@ -63,7 +64,7 @@ public class DataiConfigAuditLogController extends BaseController
return vo;
}).collect(Collectors.toList());
return getDataTable(voList);
return getDataTableByPage(voList,PageUtils.getTotal(list));
}
/**

View File

@ -3,6 +3,7 @@ package com.datai.setting.controller;
import java.util.List;
import java.util.stream.Collectors;
import com.datai.common.utils.PageUtils;
import com.datai.setting.model.vo.DataiConfigEnvironmentVo;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
@ -55,7 +56,7 @@ public class DataiConfigEnvironmentController extends BaseController
DataiConfigEnvironment dataiConfigEnvironment = DataiConfigEnvironmentDto.toObj(dataiConfigEnvironmentDto);
List<DataiConfigEnvironment> list = dataiConfigEnvironmentService.selectDataiConfigEnvironmentList(dataiConfigEnvironment);
List<DataiConfigEnvironmentVo> voList = list.stream().map(DataiConfigEnvironmentVo::objToVo).collect(Collectors.toList());
return getDataTable(voList);
return getDataTableByPage(voList,PageUtils.getTotal(list));
}
/**

View File

@ -4,6 +4,7 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.datai.common.utils.PageUtils;
import com.datai.setting.model.dto.DataiConfigSnapshotDto;
import com.datai.setting.model.vo.DataiConfigSnapshotVo;
import com.datai.setting.service.IDataiConfigEnvironmentService;
@ -80,7 +81,7 @@ public class DataiConfigSnapshotController extends BaseController
})
.collect(Collectors.toList());
return getDataTable(voList);
return getDataTableByPage(voList,PageUtils.getTotal(list));
}
/**

View File

@ -4,6 +4,7 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.datai.common.utils.PageUtils;
import com.datai.setting.model.vo.DataiConfigurationVo;
import com.datai.setting.service.IDataiConfigEnvironmentService;
import jakarta.servlet.http.HttpServletResponse;
@ -80,7 +81,7 @@ public class DataiConfigurationController extends BaseController
})
.collect(Collectors.toList());
return getDataTable(voList);
return getDataTableByPage(voList,PageUtils.getTotal(list));
}
/**