diff --git a/src/main/java/com/budwk/app/zhgh/personGovernance/apiConfig/model/PersonApiSyncOptions.java b/src/main/java/com/budwk/app/zhgh/personGovernance/apiConfig/model/PersonApiSyncOptions.java index 382a18b..1733796 100644 --- a/src/main/java/com/budwk/app/zhgh/personGovernance/apiConfig/model/PersonApiSyncOptions.java +++ b/src/main/java/com/budwk/app/zhgh/personGovernance/apiConfig/model/PersonApiSyncOptions.java @@ -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 rules = new ArrayList<>(); + public Map fieldDefaults() { + Map 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; } } diff --git a/src/main/java/com/budwk/app/zhgh/personGovernance/apiConfig/service/PersonApiConfigService.java b/src/main/java/com/budwk/app/zhgh/personGovernance/apiConfig/service/PersonApiConfigService.java index ed72914..fbce7fc 100644 --- a/src/main/java/com/budwk/app/zhgh/personGovernance/apiConfig/service/PersonApiConfigService.java +++ b/src/main/java/com/budwk/app/zhgh/personGovernance/apiConfig/service/PersonApiConfigService.java @@ -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 { NutMap syncOptionChoices(); + Sys_dict findUserFieldDictionary(String code); + void deleteConfig(String id); List tableNames(); diff --git a/src/main/java/com/budwk/app/zhgh/personGovernance/apiConfig/service/impl/PersonApiConfigServiceImpl.java b/src/main/java/com/budwk/app/zhgh/personGovernance/apiConfig/service/impl/PersonApiConfigServiceImpl.java index 302e470..f52800f 100644 --- a/src/main/java/com/budwk/app/zhgh/personGovernance/apiConfig/service/impl/PersonApiConfigServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/personGovernance/apiConfig/service/impl/PersonApiConfigServiceImpl.java @@ -54,6 +54,8 @@ import java.util.regex.Pattern; @IocBean(args = {"refer:dao"}) public class PersonApiConfigServiceImpl extends BaseServiceImpl 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 @Aop(TransAop.READ_COMMITTED) public PersonApiConfig saveConfig(PersonApiConfig config, List mappings, List params) { + return saveConfig(config, mappings, params, null); + } + + private PersonApiConfig saveConfig(PersonApiConfig config, List mappings, + List params, PersonApiSyncOptions options) { + Map 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 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 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 } 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 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 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 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 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 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 .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 defaults) { + return Strings.isNotBlank(mapping.getSourceField()) + || (mapping.getTargetField() != null && defaults.containsKey(mapping.getTargetField())); + } + private void validateSyncOptions(PersonApiConfig config, List mappings, PersonApiSyncOptions options) { if (Strings.isNotBlank(options.getUnitConfigId())) { PersonApiConfig unitConfig = fetch(options.getUnitConfigId()); @@ -311,29 +384,40 @@ public class PersonApiConfigServiceImpl extends BaseServiceImpl && !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> 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 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 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 diff --git a/src/main/java/com/budwk/app/zhgh/personGovernance/apiConfig/service/impl/PersonGovernanceSyncServiceImpl.java b/src/main/java/com/budwk/app/zhgh/personGovernance/apiConfig/service/impl/PersonGovernanceSyncServiceImpl.java index 8db2705..54cfe96 100644 --- a/src/main/java/com/budwk/app/zhgh/personGovernance/apiConfig/service/impl/PersonGovernanceSyncServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/personGovernance/apiConfig/service/impl/PersonGovernanceSyncServiceImpl.java @@ -101,6 +101,7 @@ public class PersonGovernanceSyncServiceImpl implements PersonGovernanceSyncServ List mappings = dao.query(PersonApiFieldMapping.class, Cnd.where("configId", "=", configId).asc("sort")); PersonApiSyncOptions options = personApiConfigService.syncOptions(configId); + Map 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 columns = validateTarget(config, mappings); + List 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> 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 validateTarget(PersonApiConfig config, List mappings) { + private List validateTarget(PersonApiConfig config, List mappings, + Map fieldDefaults) { Assert.hasText(config.getTargetTable(), "请选择目标人员中间表"); List columns = personApiConfigService.columns(config.getTargetTable()); Map 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 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 mappings, List columns, List rows, PersonSyncBatch batch, String lockKey, String owner, - PersonApiSyncOptions options, Map> dictionaryValues, Set unitIds) { + PersonApiSyncOptions options, Map> dictionaryValues, Set unitIds, + Map fieldDefaults) { Assert.isTrue((long) batch.getSourceCount() + rows.size() <= MAX_ROWS, "超过单批最大行数,已终止同步"); batch.setSourceCount(batch.getSourceCount() + rows.size()); Map 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()); } diff --git a/src/main/resources/views/platform/personGovernance/apiConfig/form.html b/src/main/resources/views/platform/personGovernance/apiConfig/form.html index 473265b..f51c15a 100644 --- a/src/main/resources/views/platform/personGovernance/apiConfig/form.html +++ b/src/main/resources/views/platform/personGovernance/apiConfig/form.html @@ -448,6 +448,9 @@ + + + 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) => { diff --git a/tools/person-governance-api-mock.js b/tools/person-governance-api-mock.js index cf815ce..075b76f 100644 --- a/tools/person-governance-api-mock.js +++ b/tools/person-governance-api-mock.js @@ -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'); });