fix: 人员数据治理迁移功能优化

This commit is contained in:
2026-11-12 10:22:37 +08:00
parent acac267b11
commit d64018d5a2
5 changed files with 208 additions and 5 deletions
@@ -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)
@@ -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);
@@ -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 })