fix: 人员数据治理迁移功能优化
This commit is contained in:
+6
@@ -144,6 +144,12 @@ public class PersonApiConfigController {
|
|||||||
return Result.success(personApiConfigService.queryThirdParty(data, "DATA"));
|
return Result.success(personApiConfigService.queryThirdParty(data, "DATA"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@At(value = "/queryFieldValues", methods = "POST")
|
||||||
|
@SaCheckPermission("data.person.governance.api.test")
|
||||||
|
public Result queryFieldValues(@Param("data") String data, @Param("fields") String fields) {
|
||||||
|
return Result.success(personApiConfigService.queryFieldValues(data, fields));
|
||||||
|
}
|
||||||
|
|
||||||
/** 保存当前接口请求及返回结果为可重复执行的测试用例。 */
|
/** 保存当前接口请求及返回结果为可重复执行的测试用例。 */
|
||||||
@At
|
@At
|
||||||
@Aop(TransAop.READ_COMMITTED)
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
|||||||
+2
@@ -47,6 +47,8 @@ public interface PersonApiConfigService extends BaseService<PersonApiConfig> {
|
|||||||
/** 使用页面当前config、params查询,返回第三方JSON值;非JSON返回原文,不写数据库。 */
|
/** 使用页面当前config、params查询,返回第三方JSON值;非JSON返回原文,不写数据库。 */
|
||||||
Object queryThirdParty(String data, String requestScope);
|
Object queryThirdParty(String data, String requestScope);
|
||||||
|
|
||||||
|
NutMap queryFieldValues(String data, String fields);
|
||||||
|
|
||||||
/** page为当前页码或起始数,pageSize为数量或区间长度;结束数=page+pageSize,返回第三方JSON。 */
|
/** page为当前页码或起始数,pageSize为数量或区间长度;结束数=page+pageSize,返回第三方JSON。 */
|
||||||
Object querySyncPage(PersonApiConfig config, List<PersonApiRequestParam> params, long page, int pageSize);
|
Object querySyncPage(PersonApiConfig config, List<PersonApiRequestParam> params, long page, int pageSize);
|
||||||
|
|
||||||
|
|||||||
+109
-4
@@ -44,6 +44,7 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -506,8 +507,12 @@ public class PersonApiConfigServiceImpl extends BaseServiceImpl<PersonApiConfig>
|
|||||||
List<PersonApiRequestParam> params = payload.get("params") == null
|
List<PersonApiRequestParam> params = payload.get("params") == null
|
||||||
? new ArrayList<>()
|
? new ArrayList<>()
|
||||||
: Json.fromJsonAsList(PersonApiRequestParam.class, Json.toJson(payload.get("params")));
|
: Json.fromJsonAsList(PersonApiRequestParam.class, Json.toJson(payload.get("params")));
|
||||||
// 状态信息仅供内部判断,查询接口不额外包装第三方业务字段;同步入口仍沿用原结构。
|
return queryResponse(config, params, requestScope, false);
|
||||||
NutMap response = executeHttp(config, params, requestScope, null);
|
}
|
||||||
|
|
||||||
|
private Object queryResponse(PersonApiConfig config, List<PersonApiRequestParam> params,
|
||||||
|
String requestScope, boolean fieldQuery) {
|
||||||
|
NutMap response = executeHttp(config, params, requestScope, null, fieldQuery);
|
||||||
Assert.isTrue(response.getBoolean("success"), response.getString("error") == null
|
Assert.isTrue(response.getBoolean("success"), response.getString("error") == null
|
||||||
? "第三方返回HTTP错误:" + response.get("status") : response.getString("error"));
|
? "第三方返回HTTP错误:" + response.get("status") : response.getString("error"));
|
||||||
String body = response.getString("data");
|
String body = response.getString("data");
|
||||||
@@ -522,6 +527,101 @@ public class PersonApiConfigServiceImpl extends BaseServiceImpl<PersonApiConfig>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NutMap queryFieldValues(String data, String fields) {
|
||||||
|
Assert.hasText(fields, "请输入查询字段");
|
||||||
|
List<String> names = java.util.Arrays.stream(fields.split("[,,]", -1))
|
||||||
|
.map(String::trim).distinct().toList();
|
||||||
|
Assert.isTrue(names.stream().noneMatch(String::isEmpty), "请输入字段名,字段之间用逗号分隔");
|
||||||
|
Assert.hasText(data, "请求配置不能为空");
|
||||||
|
NutMap payload = Json.fromJson(NutMap.class, data);
|
||||||
|
Assert.notNull(payload, "请求配置格式不正确");
|
||||||
|
PersonApiConfig config = Json.fromJson(PersonApiConfig.class, Json.toJson(payload.get("config")));
|
||||||
|
Assert.notNull(config, "请求配置不能为空");
|
||||||
|
List<PersonApiRequestParam> params = payload.get("params") == null ? List.of()
|
||||||
|
: Json.fromJsonAsList(PersonApiRequestParam.class, Json.toJson(payload.get("params")));
|
||||||
|
|
||||||
|
setFieldQueryPage(config, params, 100);
|
||||||
|
Object firstPage = queryResponse(config, params, "DATA", true);
|
||||||
|
String totalPath = Strings.isBlank(config.getResponseTotalPath()) ? "data->total" : config.getResponseTotalPath();
|
||||||
|
int total = fieldQueryTotal(firstPage, totalPath);
|
||||||
|
if (total == 0) {
|
||||||
|
return NutMap.NEW().addv("fields", names).addv("rows", List.of()).addv("total", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFieldQueryPage(config, params, total);
|
||||||
|
Object response = queryResponse(config, params, "DATA", true);
|
||||||
|
int returnedTotal = fieldQueryTotal(response, totalPath);
|
||||||
|
String listPath = config.getResponseDataPath();
|
||||||
|
if (Strings.isBlank(listPath) || "$.data".equals(listPath.trim()) || "data".equals(listPath.trim())) {
|
||||||
|
Object configured = readResponsePath(response, listPath);
|
||||||
|
if (!(configured instanceof List<?>)) listPath = "data->list";
|
||||||
|
}
|
||||||
|
Object dataset = readResponsePath(response, listPath);
|
||||||
|
Assert.isTrue(dataset instanceof List<?>, "数据列表路径未找到数组:" + listPath);
|
||||||
|
List<?> records = (List<?>) dataset;
|
||||||
|
Assert.isTrue(records.size() >= Math.max(total, returnedTotal),
|
||||||
|
"接口未返回全部数据,预计 " + Math.max(total, returnedTotal) + " 条,实际返回 " + records.size() + " 条");
|
||||||
|
|
||||||
|
LinkedHashSet<List<Object>> groups = new LinkedHashSet<>();
|
||||||
|
LinkedHashSet<String> found = new LinkedHashSet<>();
|
||||||
|
for (Object record : records) {
|
||||||
|
Assert.isTrue(record instanceof Map<?, ?>, "数据列表中的记录必须为对象");
|
||||||
|
Map<?, ?> row = (Map<?, ?>) record;
|
||||||
|
List<Object> values = new ArrayList<>(names.size());
|
||||||
|
for (String name : names) {
|
||||||
|
if (row.containsKey(name)) found.add(name);
|
||||||
|
values.add(row.get(name));
|
||||||
|
}
|
||||||
|
groups.add(values);
|
||||||
|
}
|
||||||
|
List<String> missing = names.stream().filter(name -> !found.contains(name)).toList();
|
||||||
|
Assert.isTrue(missing.isEmpty(), "返回数据中找不到字段:" + String.join("、", missing));
|
||||||
|
return NutMap.NEW().addv("fields", names).addv("rows", new ArrayList<>(groups)).addv("total", records.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int fieldQueryTotal(Object response, String path) {
|
||||||
|
if (response instanceof Map<?, ?> map) {
|
||||||
|
Assert.isTrue(!Boolean.FALSE.equals(map.get("status")), "数据接口返回失败:" + Strings.sNull(map.get("message")));
|
||||||
|
}
|
||||||
|
Object value = readResponsePath(response, path);
|
||||||
|
try {
|
||||||
|
int total = new java.math.BigDecimal(String.valueOf(value)).intValueExact();
|
||||||
|
if (total >= 0) return total;
|
||||||
|
} catch (NumberFormatException | ArithmeticException ignored) {
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("返回总数必须为非负整数,请检查路径:" + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setFieldQueryPage(PersonApiConfig config, List<PersonApiRequestParam> params, int pageSize) {
|
||||||
|
boolean sendBody = !List.of("GET", "HEAD").contains(Strings.sNull(config.getRequestMethod()).toUpperCase(Locale.ROOT));
|
||||||
|
Map<String, Integer> paging = Map.of("pageNumber", 1, "pageSize", pageSize);
|
||||||
|
LinkedHashSet<String> replaced = new LinkedHashSet<>();
|
||||||
|
if (sendBody && "JSON".equals(config.getBodyType()) && Strings.isNotBlank(config.getBodyContent())) {
|
||||||
|
Object body = parseResponseObject(config.getBodyContent());
|
||||||
|
if (body instanceof com.alibaba.fastjson.JSONObject json) {
|
||||||
|
paging.forEach((name, value) -> {
|
||||||
|
if (json.containsKey(name)) {
|
||||||
|
json.put(name, value);
|
||||||
|
replaced.add(name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
config.setBodyContent(com.alibaba.fastjson.JSON.toJSONString(json,
|
||||||
|
com.alibaba.fastjson.serializer.SerializerFeature.WriteMapNullValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (PersonApiRequestParam param : params) {
|
||||||
|
boolean sent = "QUERY".equals(param.getParamLocation())
|
||||||
|
|| (sendBody && "FORM_DATA".equals(config.getBodyType()) && "BODY".equals(param.getParamLocation()));
|
||||||
|
if (sent && "DATA".equals(normalizeScope(param.getRequestScope()))
|
||||||
|
&& param.getParamName() != null && paging.containsKey(param.getParamName())) {
|
||||||
|
param.setParamValue(String.valueOf(paging.get(param.getParamName())));
|
||||||
|
replaced.add(param.getParamName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Assert.isTrue(replaced.size() == paging.size(), "请在数据请求参数中配置 pageNumber 和 pageSize");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 同步取数:params是保存的两套参数,page为页码或起点,pageSize为数量或区间长度;仅替换请求副本。
|
* 同步取数:params是保存的两套参数,page为页码或起点,pageSize为数量或区间长度;仅替换请求副本。
|
||||||
* Access_Token标记传入缓存Token,参数值可使用Bearer ${Access_Token}保留前缀。
|
* Access_Token标记传入缓存Token,参数值可使用Bearer ${Access_Token}保留前缀。
|
||||||
@@ -788,9 +888,14 @@ public class PersonApiConfigServiceImpl extends BaseServiceImpl<PersonApiConfig>
|
|||||||
/** 已保存测试和页面查询共享HTTP组装;校验仅在实际请求时执行,响应不自动落库。 */
|
/** 已保存测试和页面查询共享HTTP组装;校验仅在实际请求时执行,响应不自动落库。 */
|
||||||
private NutMap executeHttp(PersonApiConfig config, List<PersonApiRequestParam> params,
|
private NutMap executeHttp(PersonApiConfig config, List<PersonApiRequestParam> params,
|
||||||
String scope, String testCondition) {
|
String scope, String testCondition) {
|
||||||
|
return executeHttp(config, params, scope, testCondition, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private NutMap executeHttp(PersonApiConfig config, List<PersonApiRequestParam> params,
|
||||||
|
String scope, String testCondition, boolean fieldQuery) {
|
||||||
Assert.isTrue(List.of("TOKEN", "DATA").contains(scope), "请求配置范围不正确");
|
Assert.isTrue(List.of("TOKEN", "DATA").contains(scope), "请求配置范围不正确");
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
boolean omitToken = "DATA".equals(scope) && Strings.isBlank(config.getTokenRequestUrl());
|
boolean omitToken = !fieldQuery && "DATA".equals(scope) && Strings.isBlank(config.getTokenRequestUrl());
|
||||||
params = params.stream()
|
params = params.stream()
|
||||||
.filter(param -> scope.equals(normalizeScope(param.getRequestScope())))
|
.filter(param -> scope.equals(normalizeScope(param.getRequestScope())))
|
||||||
// 未配置Token接口时,Header、Query及Form Data中的Token参数均不进入实际请求。
|
// 未配置Token接口时,Header、Query及Form Data中的Token参数均不进入实际请求。
|
||||||
@@ -855,7 +960,7 @@ public class PersonApiConfigServiceImpl extends BaseServiceImpl<PersonApiConfig>
|
|||||||
try (HttpResponse response = request.execute()) {
|
try (HttpResponse response = request.execute()) {
|
||||||
String responseBody = response.body();
|
String responseBody = response.body();
|
||||||
// 按排查要求打印第三方响应原文;返回值复用同一正文,不附加请求参数或统计信息。
|
// 按排查要求打印第三方响应原文;返回值复用同一正文,不附加请求参数或统计信息。
|
||||||
Logs.get().info(responseBody);
|
if (!fieldQuery) Logs.get().info(responseBody);
|
||||||
return NutMap.NEW()
|
return NutMap.NEW()
|
||||||
.addv("success", response.isOk())
|
.addv("success", response.isOk())
|
||||||
.addv("status", response.getStatus())
|
.addv("status", response.getStatus())
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
:loading="queryLoading[activeEndpoint]"
|
:loading="queryLoading[activeEndpoint]"
|
||||||
@click="testApi"
|
@click="testApi"
|
||||||
>查询</el-button>
|
>查询</el-button>
|
||||||
|
<el-button v-if="activeEndpoint==='DATA'" @click="openFieldValueQuery">查询字段值</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -439,6 +440,24 @@
|
|||||||
@click="save"
|
@click="save"
|
||||||
>保存配置</el-button>
|
>保存配置</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
<el-dialog title="查询字段值" :visible.sync="fieldValueQueryVisible" append-to-body width="760px"
|
||||||
|
:close-on-click-modal="false" custom-class="person-governance-dialog">
|
||||||
|
<el-input v-model="fieldValueQueryInput" :disabled="fieldValueQueryLoading" clearable
|
||||||
|
placeholder="输入返回字段名,多个字段用逗号分隔,例如 xb,mz" @keyup.enter.native="queryFieldValues"></el-input>
|
||||||
|
<div v-if="fieldValueQueryResult" style="margin-top: 16px">
|
||||||
|
<p>读取 {{ fieldValueQueryResult.total }} 条记录,共 {{ fieldValueQueryResult.rows.length }} 组不同值</p>
|
||||||
|
<el-table :data="fieldValueQueryResult.rows" border max-height="420" empty-text="接口没有返回数据">
|
||||||
|
<el-table-column v-for="(field, index) in fieldValueQueryResult.fields" :key="field" :label="field"
|
||||||
|
min-width="120" show-overflow-tooltip>
|
||||||
|
<template slot-scope="scope">{{ formatFieldValue(scope.row[index]) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
<div slot="footer">
|
||||||
|
<el-button @click="fieldValueQueryVisible=false">关闭</el-button>
|
||||||
|
<el-button type="primary" :loading="fieldValueQueryLoading" @click="queryFieldValues">查询</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
<el-dialog title="字段配置" :visible.sync="fieldRuleVisible" append-to-body width="520px"
|
<el-dialog title="字段配置" :visible.sync="fieldRuleVisible" append-to-body width="520px"
|
||||||
:close-on-click-modal="false" custom-class="person-governance-dialog">
|
:close-on-click-modal="false" custom-class="person-governance-dialog">
|
||||||
<el-form :model="fieldRuleDraft" label-width="120px">
|
<el-form :model="fieldRuleDraft" label-width="120px">
|
||||||
@@ -543,6 +562,10 @@
|
|||||||
testResults: { TOKEN: '', DATA: '' },
|
testResults: { TOKEN: '', DATA: '' },
|
||||||
responseWarnings: { TOKEN: '', DATA: '' },
|
responseWarnings: { TOKEN: '', DATA: '' },
|
||||||
queryLoading: { TOKEN: false, DATA: false },
|
queryLoading: { TOKEN: false, DATA: false },
|
||||||
|
fieldValueQueryVisible: false,
|
||||||
|
fieldValueQueryInput: '',
|
||||||
|
fieldValueQueryLoading: false,
|
||||||
|
fieldValueQueryResult: null,
|
||||||
cronValidating: false,
|
cronValidating: false,
|
||||||
cronDialogVisible: false,
|
cronDialogVisible: false,
|
||||||
cronDialogLoading: false,
|
cronDialogLoading: false,
|
||||||
@@ -1180,6 +1203,39 @@
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
openFieldValueQuery() {
|
||||||
|
this.$set(this, 'fieldValueQueryVisible', true)
|
||||||
|
},
|
||||||
|
queryFieldValues() {
|
||||||
|
if (this.fieldValueQueryLoading) return
|
||||||
|
const fields = this.fieldValueQueryInput.trim()
|
||||||
|
if (!fields) {
|
||||||
|
this.notifyWarning('请输入查询字段')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$set(this, 'fieldValueQueryLoading', true)
|
||||||
|
this.$set(this, 'fieldValueQueryResult', null)
|
||||||
|
$.post(apiBase + '/queryFieldValues', {
|
||||||
|
fields,
|
||||||
|
data: JSON.stringify({
|
||||||
|
config: this.form,
|
||||||
|
params: this.params.filter((param) => param.requestScope === 'DATA'),
|
||||||
|
}),
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.code === 0) {
|
||||||
|
this.$set(this, 'fieldValueQueryResult', res.data)
|
||||||
|
} else {
|
||||||
|
this.notifyWarning(res.msg || '查询字段值失败')
|
||||||
|
}
|
||||||
|
}).fail(() => {
|
||||||
|
this.notifyWarning('查询字段值失败,请检查网络和接口配置')
|
||||||
|
}).always(() => {
|
||||||
|
this.$set(this, 'fieldValueQueryLoading', false)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
formatFieldValue(value) {
|
||||||
|
return typeof value === 'string' && value ? value : JSON.stringify(value)
|
||||||
|
},
|
||||||
testApi() {
|
testApi() {
|
||||||
// 捕获发起时的页签:请求期间切换页签,响应仍写回原来的范围。
|
// 捕获发起时的页签:请求期间切换页签,响应仍写回原来的范围。
|
||||||
const scope = this.activeEndpoint
|
const scope = this.activeEndpoint
|
||||||
|
|||||||
@@ -51,9 +51,10 @@
|
|||||||
{{row.enabled ? '是' : '否'}}
|
{{row.enabled ? '是' : '否'}}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="130">
|
<el-table-column label="操作" width="190">
|
||||||
<template v-slot="{row}">
|
<template v-slot="{row}">
|
||||||
<el-button type="text" @click="edit(row)">编辑</el-button>
|
<el-button type="text" @click="edit(row)">编辑</el-button>
|
||||||
|
<el-button type="text" :loading="!!syncingRows[row.id]" @click="syncOnce(row)">执行一次</el-button>
|
||||||
<el-button type="text" class="text-danger" @click="remove(row)">删除</el-button>
|
<el-button type="text" class="text-danger" @click="remove(row)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -102,6 +103,7 @@
|
|||||||
pageLeaving: false,
|
pageLeaving: false,
|
||||||
typeVisible: false,
|
typeVisible: false,
|
||||||
pendingCreateType: '',
|
pendingCreateType: '',
|
||||||
|
syncingRows: {},
|
||||||
pageForm: { configName: '', sourceType: '', enabled: '', pageNumber: 1, pageSize: 10, totalCount: 0 },
|
pageForm: { configName: '', sourceType: '', enabled: '', pageNumber: 1, pageSize: 10, totalCount: 0 },
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -183,6 +185,38 @@
|
|||||||
edit(row) {
|
edit(row) {
|
||||||
this.leavePage('/platform/personGovernance/apiConfig/form?id=' + row.id)
|
this.leavePage('/platform/personGovernance/apiConfig/form?id=' + row.id)
|
||||||
},
|
},
|
||||||
|
async syncOnce(row) {
|
||||||
|
if (this.syncingRows[row.id]) return
|
||||||
|
this.$set(this.syncingRows, row.id, true)
|
||||||
|
try {
|
||||||
|
await this.$confirm('确认执行一次「' + row.configName + '」同步?本次执行会写入目标表。', '执行一次', {
|
||||||
|
confirmButtonText: '执行',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
} catch (cancelled) {
|
||||||
|
this.$delete(this.syncingRows, row.id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await $.post(loc() + '/sync', { id: row.id })
|
||||||
|
if (res.code !== 0) {
|
||||||
|
this.notifyWarning(res.msg || '同步执行失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const batch = res.data
|
||||||
|
if (!batch || batch.status !== 'SUCCESS') {
|
||||||
|
this.notifyWarning((batch && batch.errorMessage) || '同步执行失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.notifySuccess('同步完成,新增 ' + (batch.newCount || 0) + ' 条,更新 ' + (batch.changeCount || 0) + ' 条')
|
||||||
|
this.pageData()
|
||||||
|
} catch (error) {
|
||||||
|
this.notifyWarning('未收到同步结果,执行状态未知')
|
||||||
|
} finally {
|
||||||
|
this.$delete(this.syncingRows, row.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
remove(row) {
|
remove(row) {
|
||||||
this.$confirm('删除后不可继续同步,确认删除吗?', '提示').then(() => {
|
this.$confirm('删除后不可继续同步,确认删除吗?', '提示').then(() => {
|
||||||
$.post(loc() + '/delete', { id: row.id })
|
$.post(loc() + '/delete', { id: row.id })
|
||||||
|
|||||||
Reference in New Issue
Block a user