【feat】 元数据变更拉取
This commit is contained in:
parent
ea83fd6f0d
commit
ecd34428de
245
doc/salesforce/integration/全对象元数据变更拉取接口文档.md
Normal file
245
doc/salesforce/integration/全对象元数据变更拉取接口文档.md
Normal 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
|
||||
@ -51,3 +51,11 @@ export function delChange(id) {
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 全对象元数据变更拉取
|
||||
export function pullAllChange() {
|
||||
return request({
|
||||
url: '/integration/change/pullAll',
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
@ -163,38 +163,101 @@
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="grid-layout">
|
||||
<el-descriptions :column="2" border direction="vertical" class="mt-4">
|
||||
<el-descriptions-item label="Instance URL">
|
||||
<el-link :href="loginResult.instanceUrl" target="_blank" type="primary" :underline="false">
|
||||
{{ loginResult.instanceUrl }} <el-icon><TopRight /></el-icon>
|
||||
<el-divider content-position="left">
|
||||
<el-icon><User /></el-icon> 用户信息
|
||||
</el-divider>
|
||||
|
||||
<el-descriptions :column="2" border class="mt-4">
|
||||
<el-descriptions-item label="用户姓名">
|
||||
<div class="user-info">
|
||||
<el-icon class="user-icon"><User /></el-icon>
|
||||
{{ loginResult.userFullName }}
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="用户邮箱">
|
||||
<el-link :href="`mailto:${loginResult.userEmail}`" type="primary" :underline="false">
|
||||
{{ loginResult.userEmail }}
|
||||
</el-link>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="用户 ID">
|
||||
<span class="mono-text">{{ loginResult.userId }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="组织名称">
|
||||
<el-tag type="info" size="small" effect="light">
|
||||
{{ loginResult.organizationName }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-divider content-position="left">
|
||||
<el-icon><Monitor /></el-icon> 环境信息
|
||||
</el-divider>
|
||||
|
||||
<el-descriptions :column="2" border class="mt-4">
|
||||
<el-descriptions-item label="Instance URL">
|
||||
<el-link :href="loginResult.instanceUrl" target="_blank" type="primary" :underline="false">
|
||||
{{ loginResult.instanceUrl }} <el-icon><TopRight /></el-icon>
|
||||
</el-link>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Environment">
|
||||
<el-tag :type="loginResult.sandbox ? 'warning' : 'success'" size="small" effect="light">
|
||||
<el-icon><Monitor /></el-icon>
|
||||
{{ loginResult.sandbox ? 'Sandbox' : 'Production' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Token Type">
|
||||
<el-tag size="small" type="primary">{{ loginResult.tokenType }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="会话状态">
|
||||
<el-tag :type="loginResult.sessionExpired ? 'danger' : 'success'" size="small" effect="light">
|
||||
<el-icon><Switch /></el-icon>
|
||||
{{ loginResult.sessionExpired ? '已过期' : '有效' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="有效时间">
|
||||
<div class="expires-info">
|
||||
<el-icon><Clock /></el-icon>
|
||||
{{ loginResult.expiresIn ? formatExpiresIn(loginResult.expiresIn) : 'Unknown' }}
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="语言/时区">
|
||||
<div class="locale-info">
|
||||
<el-tag size="small" effect="plain">{{ loginResult.language }}</el-tag>
|
||||
<span class="timezone-text">{{ loginResult.timeZone }}</span>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-divider content-position="left">
|
||||
<el-icon><Link /></el-icon> 服务端点
|
||||
</el-divider>
|
||||
|
||||
<div class="metadata-link" v-if="loginResult.metadataServerUrl">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="Metadata Server URL">
|
||||
<el-link :href="loginResult.metadataServerUrl" target="_blank" type="info" :underline="false">
|
||||
{{ loginResult.metadataServerUrl }} <el-icon><TopRight /></el-icon>
|
||||
</el-link>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="User ID">
|
||||
<span class="mono-text">{{ loginResult.userId }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Org ID">
|
||||
<span class="mono-text">{{ loginResult.organizationId }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Token Type">
|
||||
<el-tag size="small">{{ loginResult.tokenType }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Environment">
|
||||
<el-tag :type="loginResult.sandbox ? 'warning' : 'success'" size="small" effect="light">
|
||||
{{ loginResult.sandbox ? 'Sandbox' : 'Production' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Expires In">
|
||||
{{ loginResult.expiresIn ? loginResult.expiresIn + ' s' : 'Unknown' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<el-divider content-position="left">Metadata Server</el-divider>
|
||||
<div class="metadata-link" v-if="loginResult.metadataServerUrl">
|
||||
<el-link :href="loginResult.metadataServerUrl" target="_blank" type="info">
|
||||
{{ loginResult.metadataServerUrl }}
|
||||
</el-link>
|
||||
|
||||
<div v-if="loginResult.refreshToken" class="refresh-token-section">
|
||||
<el-alert
|
||||
title="Refresh Token Available"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
>
|
||||
<template #default>
|
||||
<div class="code-block">
|
||||
{{ loginResult.refreshToken }}
|
||||
<el-button link type="primary" @click="copyText(loginResult.refreshToken)">
|
||||
<el-icon><DocumentCopy /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -222,7 +285,7 @@
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import {
|
||||
RefreshRight, User, SwitchButton, DocumentCopy, Switch,
|
||||
Connection, Key, Lock, Ticket, Hide, Monitor, TopRight, ArrowDown, Link
|
||||
Connection, Key, Lock, Ticket, Hide, Monitor, TopRight, ArrowDown, Link, Clock
|
||||
} from '@element-plus/icons-vue';
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
@ -478,6 +541,22 @@ const handleLoginTypeChange = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const formatExpiresIn = (seconds) => {
|
||||
if (!seconds) return 'Unknown';
|
||||
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = seconds % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}小时 ${minutes}分钟 ${secs}秒`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}分钟 ${secs}秒`;
|
||||
} else {
|
||||
return `${secs}秒`;
|
||||
}
|
||||
};
|
||||
|
||||
const copyText = (text) => {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(text).then(() => ElMessage.success('已复制 Session ID'));
|
||||
@ -633,6 +712,43 @@ const handleLoginTypeChange = () => {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 用户信息样式 */
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.user-icon {
|
||||
color: #409eff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 过期信息样式 */
|
||||
.expires-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
/* 语言/时区样式 */
|
||||
.locale-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.timezone-text {
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Refresh Token 区域样式 */
|
||||
.refresh-token-section {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.main-row {
|
||||
|
||||
@ -117,6 +117,16 @@
|
||||
v-hasPermi="['integration:change:export']"
|
||||
>导出</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="success"
|
||||
plain
|
||||
icon="Refresh"
|
||||
:loading="pullLoading"
|
||||
@click="handlePullAll"
|
||||
v-hasPermi="['integration:change:pullAll']"
|
||||
>变更拉取</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
@ -498,12 +508,14 @@
|
||||
</template>
|
||||
|
||||
<script setup name="Change">
|
||||
import { listChange, getChange, delChange, addChange, updateChange } from "@/api/integration/change";
|
||||
import { listChange, getChange, delChange, addChange, updateChange, pullAllChange } from "@/api/integration/change";
|
||||
import { ElNotification } from 'element-plus';
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const changeList = ref([]);
|
||||
const open = ref(false);
|
||||
const loading = ref(true);
|
||||
const pullLoading = ref(false);
|
||||
const showSearch = ref(true);
|
||||
const ids = ref([]);
|
||||
const single = ref(true);
|
||||
@ -743,5 +755,33 @@
|
||||
}, `change_${new Date().getTime()}.xlsx`)
|
||||
}
|
||||
|
||||
/** 全对象元数据变更拉取按钮操作 */
|
||||
function handlePullAll() {
|
||||
proxy.$modal.confirm('确认要执行全对象元数据变更拉取吗?此操作可能需要较长时间。').then(() => {
|
||||
pullLoading.value = true;
|
||||
|
||||
const notification = ElNotification({
|
||||
title: '正在拉取变更数据',
|
||||
message: '正在从 Salesforce 拉取全对象元数据变更,请稍候...',
|
||||
type: 'info',
|
||||
duration: 0,
|
||||
showClose: false
|
||||
});
|
||||
|
||||
return pullAllChange().then(response => {
|
||||
notification.close();
|
||||
proxy.$modal.msgSuccess(response.data.message || '变更拉取成功');
|
||||
getList();
|
||||
}).catch(error => {
|
||||
notification.close();
|
||||
throw error;
|
||||
});
|
||||
}).catch(() => {
|
||||
pullLoading.value = false;
|
||||
}).finally(() => {
|
||||
pullLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
getList();
|
||||
</script>
|
||||
Loading…
Reference in New Issue
Block a user