fix: 人员数据治理迁移8

This commit is contained in:
2026-11-11 17:29:54 +08:00
parent 6aabaf7e52
commit acac267b11
6 changed files with 193 additions and 54 deletions
@@ -3,7 +3,9 @@ package com.budwk.app.zhgh.personGovernance.apiConfig.model;
import lombok.Data;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Data
public class PersonApiSyncOptions {
@@ -11,11 +13,22 @@ public class PersonApiSyncOptions {
private String unitConfigId;
private List<FieldRule> rules = new ArrayList<>();
public Map<String, String> fieldDefaults() {
Map<String, String> defaults = new HashMap<>();
for (FieldRule rule : rules) {
if (rule.getDefaultValue() != null && !rule.getDefaultValue().isEmpty()) {
defaults.put(rule.getTargetField(), rule.getDefaultValue());
}
}
return defaults;
}
@Data
public static class FieldRule {
private String sourceField;
private String targetField;
private Integer length;
private String dictCode;
private String defaultValue;
}
}
@@ -3,6 +3,7 @@ package com.budwk.app.zhgh.personGovernance.apiConfig.service;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.zhgh.personGovernance.apiConfig.model.PersonApiConfig;
import com.budwk.app.zhgh.personGovernance.apiConfig.model.PersonApiSyncOptions;
import com.budwk.app.zhgh.personGovernance.apiConfig.model.PersonApiFieldMapping;
@@ -33,6 +34,8 @@ public interface PersonApiConfigService extends BaseService<PersonApiConfig> {
NutMap syncOptionChoices();
Sys_dict findUserFieldDictionary(String code);
void deleteConfig(String id);
List<NutMap> tableNames();
@@ -54,6 +54,8 @@ import java.util.regex.Pattern;
@IocBean(args = {"refer:dao"})
public class PersonApiConfigServiceImpl extends BaseServiceImpl<PersonApiConfig> implements PersonApiConfigService {
private static final Pattern SAFE_IDENTIFIER = Pattern.compile("^[A-Za-z0-9_]+$");
private static final String SYNC_OPTIONS_CHUNKS = "@chunks:";
private static final String USER_FIELD_DICTIONARIES = "USER_TABLE_COLUMN_DICTS";
/** JSON节点删除标记:Token接口未配置时,用于彻底移除数据请求中的Token字段或数组项。 */
private static final Object OMIT_TOKEN_PARAMETER = new Object();
@@ -124,6 +126,12 @@ public class PersonApiConfigServiceImpl extends BaseServiceImpl<PersonApiConfig>
@Aop(TransAop.READ_COMMITTED)
public PersonApiConfig saveConfig(PersonApiConfig config, List<PersonApiFieldMapping> mappings,
List<PersonApiRequestParam> params) {
return saveConfig(config, mappings, params, null);
}
private PersonApiConfig saveConfig(PersonApiConfig config, List<PersonApiFieldMapping> mappings,
List<PersonApiRequestParam> params, PersonApiSyncOptions options) {
Map<String, String> defaults = options == null ? java.util.Collections.emptyMap() : options.fieldDefaults();
Assert.notNull(config, "配置不能为空");
Assert.hasText(config.getConfigName(), "请填写配置名称");
PersonApiConfig stored = Strings.isBlank(config.getId()) ? null : fetch(config.getId());
@@ -146,8 +154,8 @@ public class PersonApiConfigServiceImpl extends BaseServiceImpl<PersonApiConfig>
mappings = mappings == null ? new ArrayList<>() : mappings;
if (Boolean.TRUE.equals(config.getEnabled())) {
Assert.hasText(config.getTargetTable(), "启用同步任务前请选择目标表");
Assert.isTrue(mappings.stream().anyMatch(mapping -> Strings.isNotBlank(mapping.getSourceField())
&& Strings.isNotBlank(mapping.getTargetField())), "启用同步任务前请至少完成一条来源字段与目标字段的映射");
Assert.isTrue(mappings.stream().anyMatch(mapping -> Strings.isNotBlank(mapping.getTargetField())
&& hasMappingValue(mapping, defaults)), "启用同步任务前请至少配置一条字段映射或默认值");
}
PersonApiFieldMapping identity = mappings.stream()
.filter(v -> Boolean.TRUE.equals(v.getIdentityFlag()))
@@ -168,7 +176,7 @@ public class PersonApiConfigServiceImpl extends BaseServiceImpl<PersonApiConfig>
Assert.isTrue(isUnitConfig(config) && "UPSERT".equals(config.getSyncMode())
&& "id".equals(config.getIdentityField()), "单位API配置必须使用存在即修改模式,并将来源单位编码映射到id作为唯一标识");
Assert.isTrue(mappings.stream().anyMatch(mapping -> "name".equals(mapping.getTargetField())
&& Strings.isNotBlank(mapping.getSourceField())), "单位API配置必须映射单位名称name");
&& hasMappingValue(mapping, defaults)), "单位API配置必须映射单位名称name");
}
if ("HTTP_API".equals(config.getSourceType())) pagination(config, params);
@@ -240,6 +248,9 @@ public class PersonApiConfigServiceImpl extends BaseServiceImpl<PersonApiConfig>
}
dao().insert(params);
}
if (options != null) {
saveSyncOptions(config.getId(), options);
}
if (previousDataQueryResultKey != null
&& !Objects.equals(previousDataQueryResultKey, config.getDataQueryResult())) {
deleteDataQueryResultQuietly(previousDataQueryResultKey);
@@ -267,32 +278,78 @@ public class PersonApiConfigServiceImpl extends BaseServiceImpl<PersonApiConfig>
if (options != null) {
validateSyncOptions(config, mappings, options);
}
PersonApiConfig saved = saveConfig(config, mappings, params);
if (options != null) {
Sys_config settings = new Sys_config();
settings.setConfigKey(syncOptionsKey(saved.getId()));
settings.setConfigValue(Json.toJson(options));
settings.setNote("人员治理API同步选项");
dao().insertOrUpdate(settings);
}
return saved;
return saveConfig(config, mappings, params, options);
}
private String syncOptionsKey(String configId) {
return "personGovernance.syncOptions." + configId;
}
private void saveSyncOptions(String configId, PersonApiSyncOptions options) {
String key = syncOptionsKey(configId);
String value = Json.toJson(options);
// 同时遵守字符列宽和文本列的字节上限,按 Unicode 码点分段以保留完整字符。
List<NutMap> column = listMap(Sqls.create("SELECT LEAST(CHARACTER_MAXIMUM_LENGTH, "
+ "FLOOR(CHARACTER_OCTET_LENGTH / 4)) valueLength FROM information_schema.COLUMNS "
+ "WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='sys_config' AND COLUMN_NAME='configValue'"));
Assert.isTrue(!column.isEmpty() && column.get(0).getInt("valueLength") > 0, "无法读取系统配置值列宽");
int limit = column.get(0).getInt("valueLength");
int remaining = value.codePointCount(0, value.length());
List<Sys_config> chunks = new ArrayList<>();
if (remaining > limit) {
int offset = 0;
while (remaining > 0) {
int size = Math.min(limit, remaining);
int end = value.offsetByCodePoints(offset, size);
chunks.add(syncOptionsRow(key + "." + (chunks.size() + 1), value.substring(offset, end)));
offset = end;
remaining -= size;
}
value = SYNC_OPTIONS_CHUNKS + chunks.size();
Assert.isTrue(value.length() <= limit, "系统配置值列宽不足以保存分段标记");
}
dao().insertOrUpdate(syncOptionsRow(key, value));
dao().clear(Sys_config.class, Cnd.where("configKey", "like", key + ".%"));
if (!chunks.isEmpty()) dao().insert(chunks);
}
private Sys_config syncOptionsRow(String key, String value) {
Sys_config settings = new Sys_config();
settings.setConfigKey(key);
settings.setConfigValue(value);
settings.setNote("人员治理API同步选项");
return settings;
}
@Override
public PersonApiSyncOptions syncOptions(String configId) {
Sys_config settings = dao().fetch(Sys_config.class, syncOptionsKey(configId));
return settings == null ? new PersonApiSyncOptions()
: Json.fromJson(PersonApiSyncOptions.class, settings.getConfigValue());
String key = syncOptionsKey(configId);
Map<String, String> values = new LinkedHashMap<>();
dao().query(Sys_config.class, Cnd.where("configKey", "=", key).or("configKey", "like", key + ".%"))
.forEach(settings -> values.put(settings.getConfigKey(), settings.getConfigValue()));
String value = values.get(key);
if (value == null) return new PersonApiSyncOptions();
if (value.startsWith(SYNC_OPTIONS_CHUNKS)) {
int chunkCount = Integer.parseInt(value.substring(SYNC_OPTIONS_CHUNKS.length()));
Assert.isTrue(chunkCount > 0 && chunkCount == values.size() - 1, "人员治理同步选项分段不完整");
StringBuilder json = new StringBuilder();
for (int i = 1; i <= chunkCount; i++) {
String chunk = values.get(key + "." + i);
Assert.notNull(chunk, "人员治理同步选项分段不完整");
json.append(chunk);
}
value = json.toString();
}
return Json.fromJson(PersonApiSyncOptions.class, value);
}
@Override
public NutMap syncOptionChoices() {
Sys_dict root = userFieldDictionaryRoot();
List<Sys_dict> dictionaries = root == null ? List.of()
: dao().query(Sys_dict.class, Cnd.where("parentId", "=", root.getId()).asc("name"));
return NutMap.NEW()
.addv("dicts", dao().query(Sys_dict.class, Cnd.where("parentId", "=", "").or("parentId", "is", null).asc("name")))
.addv("dicts", dictionaries)
.addv("unitConfigs", dao().query(PersonApiConfig.class,
Cnd.where("sourceType", "=", "HTTP_API").and("targetTable", "=", "sys_unit")
.and("syncMode", "=", "UPSERT").and("identityField", "=", "id")
@@ -300,10 +357,26 @@ public class PersonApiConfigServiceImpl extends BaseServiceImpl<PersonApiConfig>
.map(item -> NutMap.NEW().addv("id", item.getId()).addv("configName", item.getConfigName())).toList());
}
private Sys_dict userFieldDictionaryRoot() {
return dao().fetch(Sys_dict.class, Cnd.where("code", "=", USER_FIELD_DICTIONARIES));
}
@Override
public Sys_dict findUserFieldDictionary(String code) {
Sys_dict root = userFieldDictionaryRoot();
return root == null ? null : dao().fetch(Sys_dict.class,
Cnd.where("parentId", "=", root.getId()).and("code", "=", code));
}
private boolean isUnitConfig(PersonApiConfig config) {
return "HTTP_API".equals(config.getSourceType()) && "sys_unit".equalsIgnoreCase(config.getTargetTable());
}
private boolean hasMappingValue(PersonApiFieldMapping mapping, Map<String, String> defaults) {
return Strings.isNotBlank(mapping.getSourceField())
|| (mapping.getTargetField() != null && defaults.containsKey(mapping.getTargetField()));
}
private void validateSyncOptions(PersonApiConfig config, List<PersonApiFieldMapping> mappings, PersonApiSyncOptions options) {
if (Strings.isNotBlank(options.getUnitConfigId())) {
PersonApiConfig unitConfig = fetch(options.getUnitConfigId());
@@ -311,29 +384,40 @@ public class PersonApiConfigServiceImpl extends BaseServiceImpl<PersonApiConfig>
&& !Boolean.TRUE.equals(unitConfig.getDelFlag()) && isUnitConfig(unitConfig)
&& "UPSERT".equals(unitConfig.getSyncMode()) && "id".equals(unitConfig.getIdentityField()),
"请选择有效的单位API配置");
Assert.isTrue(mappings != null && mappings.stream().anyMatch(mapping -> "unitId".equals(mapping.getTargetField())
&& Strings.isNotBlank(mapping.getSourceField())), "自动同步单位需要配置unitId映射");
}
Assert.notNull(options.getRules(), "字段配置不能为空");
java.util.Set<List<String>> ruleFields = new java.util.HashSet<>();
for (PersonApiSyncOptions.FieldRule rule : options.getRules()) {
Assert.notNull(rule, "字段配置不能为空");
Assert.hasText(rule.getSourceField(), "请填写配置规则的来源字段");
Assert.isTrue(ruleFields.add(List.of(rule.getSourceField(), Strings.sNull(rule.getTargetField()))),
boolean hasDefault = rule.getDefaultValue() != null && !rule.getDefaultValue().isEmpty();
if (rule.getLength() != null || !hasDefault) {
Assert.hasText(rule.getSourceField(), "请填写配置规则的来源字段");
}
Assert.isTrue(ruleFields.add(List.of(Strings.sNull(rule.getSourceField()), Strings.sNull(rule.getTargetField()))),
"同一来源字段和目标字段只能配置一条规则");
Assert.isTrue(rule.getLength() == null || rule.getLength() >= 0, "字段长度必须为非负整数");
Assert.isTrue(mappings != null && mappings.stream().anyMatch(mapping ->
Objects.equals(mapping.getSourceField(), rule.getSourceField())
&& Objects.equals(Strings.sNull(mapping.getTargetField()), Strings.sNull(rule.getTargetField()))),
"字段规则必须关联一条字段映射");
PersonApiFieldMapping mapping = mappings == null ? null : mappings.stream().filter(item ->
Objects.equals(Strings.sNull(item.getSourceField()), Strings.sNull(rule.getSourceField()))
&& Objects.equals(Strings.sNull(item.getTargetField()), Strings.sNull(rule.getTargetField())))
.findFirst().orElse(null);
Assert.notNull(mapping, "字段规则必须关联一条字段映射");
if (hasDefault) {
Assert.hasText(rule.getTargetField(), "设置默认值前请选择目标字段");
Assert.isTrue(!List.of("id", "pullTime").contains(rule.getTargetField())
&& !Boolean.TRUE.equals(mapping.getIdentityFlag()), "主键、批次时间和唯一标识不能设置默认值");
}
if (Strings.isNotBlank(rule.getDictCode())) {
Assert.hasText(rule.getTargetField(), "选择字典前请填写目标字段");
Assert.isTrue(!"pullTime".equals(rule.getTargetField())
&& (!"id".equals(rule.getTargetField()) || isUnitConfig(config)), "系统生成字段不能配置字典");
Assert.notNull(dao().fetch(Sys_dict.class, Cnd.where("code", "=", rule.getDictCode())
.and(Cnd.exps("parentId", "=", "").or("parentId", "is", null))), "字典分类不存在");
Assert.notNull(findUserFieldDictionary(rule.getDictCode()), "用户表字段字典下不存在该分类:" + rule.getDictCode());
}
}
if (Strings.isNotBlank(options.getUnitConfigId())) {
Map<String, String> defaults = options.fieldDefaults();
Assert.isTrue(mappings != null && mappings.stream().anyMatch(mapping -> "unitId".equals(mapping.getTargetField())
&& hasMappingValue(mapping, defaults)), "自动同步单位需要配置unitId映射");
}
}
@Override
@@ -350,7 +434,8 @@ public class PersonApiConfigServiceImpl extends BaseServiceImpl<PersonApiConfig>
PersonApiConfig config = fetch(id);
config.setEnabled(false);
reconcileTask(config);
dao().delete(Sys_config.class, syncOptionsKey(id));
String key = syncOptionsKey(id);
dao().clear(Sys_config.class, Cnd.where("configKey", "=", key).or("configKey", "like", key + ".%"));
}
@Override
@@ -101,6 +101,7 @@ public class PersonGovernanceSyncServiceImpl implements PersonGovernanceSyncServ
List<PersonApiFieldMapping> mappings = dao.query(PersonApiFieldMapping.class,
Cnd.where("configId", "=", configId).asc("sort"));
PersonApiSyncOptions options = personApiConfigService.syncOptions(configId);
Map<String, String> fieldDefaults = options.fieldDefaults();
boolean unitTarget = "sys_unit".equalsIgnoreCase(config.getTargetTable());
if (unitTarget) {
// 单位写入可能随父批次一起提交,行锁一直持有到外层事务完成。
@@ -117,9 +118,10 @@ public class PersonGovernanceSyncServiceImpl implements PersonGovernanceSyncServ
rule.getLength() != null && Strings.isBlank(rule.getTargetField())
&& Objects.equals(rule.getSourceField(), m.getSourceField())))
.filter(m -> Boolean.TRUE.equals(m.getRequired())
|| (Strings.isNotBlank(m.getSourceField()) && Strings.isNotBlank(m.getTargetField())))
|| (Strings.isNotBlank(m.getTargetField())
&& (Strings.isNotBlank(m.getSourceField()) || fieldDefaults.containsKey(m.getTargetField()))))
.toList();
List<NutMap> columns = validateTarget(config, mappings);
List<NutMap> columns = validateTarget(config, mappings, fieldDefaults);
log.info("[人员同步] 目标表和字段映射校验完成,configId={},batchNo={},targetTable={},mappingCount={},columnCount={}",
configId, batch.getBatchNo(), config.getTargetTable(), mappings.size(), columns.size());
if ("SYSTEM_TABLE".equals(config.getSourceType())) {
@@ -161,7 +163,7 @@ public class PersonGovernanceSyncServiceImpl implements PersonGovernanceSyncServ
}
log.info("[人员同步] 开始字段映射转换及目标表批量写入,configId={},batchNo={},sourceCount={},mappingCount={}",
configId, batch.getBatchNo(), rows.size(), mappings.size());
insertPage(config, mappings, columns, rows, batch, lockKey, owner, options, dictionaryValues, unitIds);
insertPage(config, mappings, columns, rows, batch, lockKey, owner, options, dictionaryValues, unitIds, fieldDefaults);
applySyncOptions(config, options, dictionaryValues, unitIds);
log.info("[人员同步] 目标表批量写入完成,configId={},batchNo={},successCount={},newCount={},changeCount={},skipCount={}",
configId, batch.getBatchNo(), batch.getSuccessCount(), batch.getNewCount(),
@@ -201,7 +203,7 @@ public class PersonGovernanceSyncServiceImpl implements PersonGovernanceSyncServ
if (!rows.isEmpty() && pageSize > 0) {
Assert.isTrue(fingerprints.add(DigestUtil.sha256Hex(Json.toJson(rows))), "接口重复返回同一页,已终止并回滚本批次");
}
insertPage(config, mappings, columns, rows, batch, lockKey, owner, options, dictionaryValues, unitIds);
insertPage(config, mappings, columns, rows, batch, lockKey, owner, options, dictionaryValues, unitIds, fieldDefaults);
// 按接口原始条数判定,已存在而跳过的记录仍属于这一页。
// 仅数量模式是单次限量查询,即使正好返回配置数量也不能继续构造下一页请求。
if (singleLimitRequest || pageSize == 0 || rows.size() < pageSize) break;
@@ -267,9 +269,8 @@ public class PersonGovernanceSyncServiceImpl implements PersonGovernanceSyncServ
}
boolean created = false;
for (Map.Entry<String, Set<String>> entry : dictionaryValues.entrySet()) {
Sys_dict parent = dao.fetch(Sys_dict.class, Cnd.where("code", "=", entry.getKey())
.and(Cnd.exps("parentId", "=", "").or("parentId", "is", null)));
Assert.notNull(parent, "字典分类不存在:" + entry.getKey());
Sys_dict parent = personApiConfigService.findUserFieldDictionary(entry.getKey());
Assert.notNull(parent, "用户表字段字典下不存在该分类:" + entry.getKey());
// 同一字典分类下串行检查和插入,避免并发同步重复创建子项。
Sql lock = Sqls.create("SELECT id FROM sys_dict WHERE id=@id FOR UPDATE").setParam("id", parent.getId());
lock.setCallback(Sqls.callback.str());
@@ -374,7 +375,8 @@ public class PersonGovernanceSyncServiceImpl implements PersonGovernanceSyncServ
}
/** 校验目标表和业务唯一标识;数据库唯一约束由批量写入检查,冲突时整批回滚。 */
private List<NutMap> validateTarget(PersonApiConfig config, List<PersonApiFieldMapping> mappings) {
private List<NutMap> validateTarget(PersonApiConfig config, List<PersonApiFieldMapping> mappings,
Map<String, String> fieldDefaults) {
Assert.hasText(config.getTargetTable(), "请选择目标人员中间表");
List<NutMap> columns = personApiConfigService.columns(config.getTargetTable());
Map<String, NutMap> fields = new HashMap<>();
@@ -387,14 +389,16 @@ public class PersonGovernanceSyncServiceImpl implements PersonGovernanceSyncServ
Assert.isTrue("HTTP_API".equals(config.getSourceType()) && "UPSERT".equals(config.getSyncMode())
&& "id".equals(config.getIdentityField()), "单位API必须按来源单位id新增或更新");
Assert.isTrue(mappings.stream().anyMatch(mapping -> "name".equals(mapping.getTargetField())
&& Strings.isNotBlank(mapping.getSourceField())), "单位API配置必须映射单位名称name");
&& (Strings.isNotBlank(mapping.getSourceField()) || fieldDefaults.containsKey("name"))),
"单位API配置必须映射单位名称name");
}
Assert.isTrue(fields.get("id").getString("columnType").toLowerCase(Locale.ROOT).contains("char"), "中间表id须支持字符串主键");
Assert.notEmpty(mappings, "请配置字段映射");
Set<String> targets = new HashSet<>();
for (PersonApiFieldMapping mapping : mappings) {
Assert.hasText(mapping.getSourceField(), "请补全来源字段");
Assert.hasText(mapping.getTargetField(), "请补全系统字段");
Assert.isTrue(Strings.isNotBlank(mapping.getSourceField()) || fieldDefaults.containsKey(mapping.getTargetField()),
"请填写来源字段或默认值:" + mapping.getTargetField());
Assert.isTrue(fields.containsKey(mapping.getTargetField()), "目标表不存在映射字段:" + mapping.getTargetField());
Assert.isTrue(targets.add(mapping.getTargetField()), "系统字段不能重复映射:" + mapping.getTargetField());
}
@@ -422,7 +426,8 @@ public class PersonGovernanceSyncServiceImpl implements PersonGovernanceSyncServ
@Aop(TransAop.READ_COMMITTED)
private void insertPage(PersonApiConfig config, List<PersonApiFieldMapping> mappings, List<NutMap> columns,
List<?> rows, PersonSyncBatch batch, String lockKey, String owner,
PersonApiSyncOptions options, Map<String, Set<String>> dictionaryValues, Set<String> unitIds) {
PersonApiSyncOptions options, Map<String, Set<String>> dictionaryValues, Set<String> unitIds,
Map<String, String> fieldDefaults) {
Assert.isTrue((long) batch.getSourceCount() + rows.size() <= MAX_ROWS, "超过单批最大行数,已终止同步");
batch.setSourceCount(batch.getSourceCount() + rows.size());
Map<String, String> types = new HashMap<>();
@@ -462,7 +467,11 @@ public class PersonGovernanceSyncServiceImpl implements PersonGovernanceSyncServ
}
NutMap values = NutMap.NEW();
for (PersonApiFieldMapping mapping : mappings) {
Object value = personApiConfigService.readResponsePath(row, mapping.getSourceField());
Object value = Strings.isBlank(mapping.getSourceField()) ? null
: personApiConfigService.readResponsePath(row, mapping.getSourceField());
if (value == null) {
value = fieldDefaults.get(mapping.getTargetField());
}
if (Boolean.TRUE.equals(mapping.getRequired()) && !(preserveUserNulls && value == null)) {
Assert.isTrue(value != null && !Strings.isBlank(String.valueOf(value)), "必填来源字段为空:" + mapping.getSourceField());
}
@@ -448,6 +448,9 @@
<el-form-item label="长度等于">
<el-input v-model="fieldRuleDraft.length" clearable placeholder="留空不过滤,例如 10"></el-input>
</el-form-item>
<el-form-item label="默认值">
<el-input v-model="fieldRuleDraft.defaultValue" clearable placeholder="未配置来源或来源值为 null 时使用"></el-input>
</el-form-item>
<el-form-item label="对应字典">
<el-select v-model="fieldRuleDraft.dictCode" clearable filterable placeholder="请选择字典分类">
<el-option v-for="item in dictChoices" :key="item.code"
@@ -529,7 +532,7 @@
dictChoices: [],
fieldRuleVisible: false,
fieldRuleRow: null,
fieldRuleDraft: { length: '', dictCode: '' },
fieldRuleDraft: { length: '', dictCode: '', defaultValue: '' },
params: [],
testCases: [],
identityRow: null,
@@ -641,7 +644,7 @@
return this.testResults[this.activeEndpoint]
},
hasMappedField() {
return this.mappings.some((v) => Boolean(v.sourceField && v.targetField))
return this.mappings.some((v) => Boolean(v.targetField && (v.sourceField || v.defaultValue)))
},
},
methods: {
@@ -663,6 +666,7 @@
this.$set(this, 'fieldRuleDraft', {
length: row.filterLength == null ? '' : String(row.filterLength),
dictCode: row.dictCode || '',
defaultValue: row.defaultValue || '',
})
this.$set(this, 'fieldRuleVisible', true)
},
@@ -670,7 +674,8 @@
const row = this.fieldRuleRow
const length = this.fieldRuleDraft.length.trim()
const dictCode = this.fieldRuleDraft.dictCode
if ((length || dictCode) && !(row.sourceField || '').trim()) {
const defaultValue = this.fieldRuleDraft.defaultValue
if ((length || (dictCode && !defaultValue)) && !(row.sourceField || '').trim()) {
this.notifyWarning('请先填写来源字段')
return
}
@@ -682,8 +687,17 @@
this.notifyWarning('选择字典前请填写目标字段')
return
}
if (defaultValue && !row.targetField) {
this.notifyWarning('设置默认值前请选择目标字段')
return
}
if (defaultValue && (['id', 'pullTime'].includes(row.targetField) || row === this.identityRow)) {
this.notifyWarning('主键、批次时间和唯一标识不能设置默认值')
return
}
this.$set(row, 'filterLength', length ? Number(length) : null)
this.$set(row, 'dictCode', dictCode)
this.$set(row, 'defaultValue', defaultValue)
this.$set(this, 'fieldRuleVisible', false)
},
openQuickMapping() {
@@ -749,6 +763,7 @@
isIdentityMapping(row) {
return Boolean(row && row.sourceField && row.sourceField.trim()
&& row.targetField && row.targetField.trim()
&& !row.defaultValue
&& row.targetField !== 'pullTime'
&& (row.targetField !== 'id' || this.form.targetTable === 'sys_unit'))
},
@@ -930,6 +945,7 @@
this.$set(row, 'targetField', '')
this.$set(row, 'targetComment', '')
this.$set(row, 'identityFlag', false)
this.$set(row, 'defaultValue', '')
})
this.$set(this, 'identityRow', null)
this.$set(this.form, 'enabled', false)
@@ -972,6 +988,7 @@
identityFlag: old ? Boolean(old.identityFlag) : false,
filterLength: old ? old.filterLength : null,
dictCode: old ? old.dictCode : '',
defaultValue: old ? old.defaultValue : '',
}
})
this.$set(this, 'mappings', rows)
@@ -1138,11 +1155,12 @@
const syncOptions = {
autoCreateDict: this.syncOptions.autoCreateDict,
unitConfigId: !unitTarget && this.autoSyncUnit ? this.syncOptions.unitConfigId : '',
rules: this.mappings.filter((row) => row.filterLength != null || row.dictCode).map((row) => ({
rules: this.mappings.filter((row) => row.filterLength != null || row.dictCode || row.defaultValue).map((row) => ({
sourceField: row.sourceField,
targetField: row.targetField,
length: row.filterLength,
dictCode: row.dictCode,
defaultValue: row.defaultValue,
})),
}
this.$set(this, 'formLoading', true)
@@ -1243,11 +1261,12 @@
this.$set(this.syncOptions, 'unitConfigId', syncOptions.unitConfigId || '')
this.$set(this, 'autoSyncUnit', Boolean(syncOptions.unitConfigId))
;(syncOptions.rules || []).forEach((rule) => {
const row = this.mappings.find((item) => item.sourceField === rule.sourceField
const row = this.mappings.find((item) => (item.sourceField || '') === (rule.sourceField || '')
&& (item.targetField || '') === (rule.targetField || ''))
if (row) {
this.$set(row, 'filterLength', rule.length)
this.$set(row, 'dictCode', rule.dictCode || '')
this.$set(row, 'defaultValue', rule.defaultValue || '')
}
})
const params = (res.data.params || []).map((v) => {
+20 -10
View File
@@ -10,14 +10,17 @@
* Access_Token 路径:data->access_token
* Expires_in 路径:data->expires_in,单位:SECONDS
* 数据地址:http://127.0.0.1:18080/api/persons,GET 或 POST
* 机构地址:http://127.0.0.1:18080/api/units,GET 或 POST
* 返回码路径:code,成功返回值:0,消息路径:msg
* 人员数组路径:data->list,总数路径:data->total
* 数据数组路径:data->list,总数路径:data->total
* 数据字段按人员接口字段表生成,全部为字符串;码值为模拟值。
* 字段映射示例:gh→loginname、xm→username、xb→sex、sjhm→mobile、yx→email、lxrq→arrivalAtSchoolDate。
* gh、grbsm 均为唯一标识,可按实际目标中间表字段配置。
* 机构配置:dwdm→id(唯一标识)、dwmc→name、bmflmc→unitType。
* 人员配置:bmmz→unitId,值与机构接口的 dwdm 相同。
* 分页 Query 参数:pageNumber=${页码}、pageSize=${数量},例如起始页码 1、数量 20。
* POST JSON 也可使用 {"pageNumber":"${页码}","pageSize":"${数量}"}。
* 不配置分页参数时一次返回全部 100 条数据。
* 也支持 page 和 pagesize。不配置分页参数时返回全部 100 条人员或 5 条机构。
* 默认不要求认证;MOCK_REQUIRE_TOKEN=1 时,数据接口要求以下任意一种凭证:
* Header Authorization: Bearer ${Access_Token}
* Query 或 JSON 参数 access_token: ${Access_Token}
@@ -32,12 +35,18 @@ const REQUIRE_TOKEN = process.env.MOCK_REQUIRE_TOKEN === '1';
const TOKEN = 'person-governance-mock-token';
const MAX_BODY_BYTES = 1024 * 1024;
const units = [
{ dwdm: 'MOCK-D1', dwmc: '计算机学院', bmflmc: '教学单位' },
{ dwdm: 'MOCK-D2', dwmc: '机械工程学院', bmflmc: '教学单位' },
{ dwdm: 'MOCK-D3', dwmc: '经济管理学院', bmflmc: '教学单位' },
{ dwdm: 'MOCK-D4', dwmc: '外国语学院', bmflmc: '教学单位' },
{ dwdm: 'MOCK-D5', dwmc: '教务处', bmflmc: '行政单位' }
];
const persons = Array.from({ length: 100 }, (_, index) => {
const number = index + 1;
const suffix = String(number).padStart(3, '0');
const department = index % 5;
const departmentCode = `MOCK-D${department + 1}`;
const departmentName = ['计算机学院', '机械工程学院', '经济管理学院', '外国语学院', '教务处'][department];
const { dwdm: departmentCode, dwmc: departmentName } = units[index % units.length];
const female = number % 2 === 0;
const retired = number % 10 === 0;
const birthYear = retired ? 1960 + index % 5 : 1980 + index % 15;
@@ -175,7 +184,7 @@ function positiveInteger(value, fallback, name) {
const server = http.createServer(async (request, response) => {
try {
const url = new URL(request.url, 'http://localhost');
if (!['/api/token', '/api/persons'].includes(url.pathname)) {
if (!['/api/token', '/api/persons', '/api/units'].includes(url.pathname)) {
request.resume();
return reply(response, 404, 404, '接口不存在');
}
@@ -196,14 +205,15 @@ const server = http.createServer(async (request, response) => {
return reply(response, 401, 401, 'Token 无效或未提供');
}
const pageNumber = positiveInteger(params.pageNumber ?? params.page, 1, 'pageNumber');
const pageSize = positiveInteger(params.pageSize, 100, 'pageSize');
const pageSize = positiveInteger(params.pageSize ?? params.pagesize, 100, 'pageSize');
if (pageSize > 1000) {
return reply(response, 400, 400, 'pageSize 不能超过 1000');
}
const start = (pageNumber - 1) * pageSize;
const records = url.pathname === '/api/units' ? units : persons;
return reply(response, 200, 0, '成功', {
list: persons.slice(start, start + pageSize),
total: persons.length,
list: records.slice(start, start + pageSize),
total: records.length,
pageNumber,
pageSize
});
@@ -220,5 +230,5 @@ server.on('error', error => {
server.listen(PORT, HOST, () => {
console.log(`人员治理测试服务:http://${HOST}:${PORT},已预填充 ${persons.length} 条模拟数据`);
console.log('Token 接口:/api/token;人员接口:/api/persons');
console.log('Token 接口:/api/token;人员接口:/api/persons;机构接口:/api/units');
});