|
|
|
@ -2,6 +2,7 @@ package apelet.common.generator.service.impl; |
|
|
|
|
|
|
|
|
|
|
|
import apelet.common.core.cache.CacheKey; |
|
|
|
import apelet.common.core.cache.CacheKey; |
|
|
|
import apelet.common.core.constant.GlobalDeletedFlag; |
|
|
|
import apelet.common.core.constant.GlobalDeletedFlag; |
|
|
|
|
|
|
|
import apelet.common.core.exception.DataNotFoundException; |
|
|
|
import apelet.common.core.object.TokenData; |
|
|
|
import apelet.common.core.object.TokenData; |
|
|
|
import apelet.common.dict.model.GlobalDictItem; |
|
|
|
import apelet.common.dict.model.GlobalDictItem; |
|
|
|
import apelet.common.dict.service.GlobalDictItemService; |
|
|
|
import apelet.common.dict.service.GlobalDictItemService; |
|
|
|
@ -51,6 +52,7 @@ import java.sql.ResultSet; |
|
|
|
import java.util.*; |
|
|
|
import java.util.*; |
|
|
|
import java.util.concurrent.ConcurrentHashMap; |
|
|
|
import java.util.concurrent.ConcurrentHashMap; |
|
|
|
import java.util.concurrent.TimeUnit; |
|
|
|
import java.util.concurrent.TimeUnit; |
|
|
|
|
|
|
|
import java.util.function.BiConsumer; |
|
|
|
import java.util.function.Function; |
|
|
|
import java.util.function.Function; |
|
|
|
import java.util.function.Predicate; |
|
|
|
import java.util.function.Predicate; |
|
|
|
import java.util.stream.Collectors; |
|
|
|
import java.util.stream.Collectors; |
|
|
|
@ -132,6 +134,55 @@ public class OnlFormHeadServiceImpl extends ServiceImpl<OnlFormHeadMapper, OnlFo |
|
|
|
redissonClient.getBucket(tableIdKey).delete(); |
|
|
|
redissonClient.getBucket(tableIdKey).delete(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* 当前请求的租户Id。B 端(管理端)为 null,A 端(租户端)为登录租户Id。 |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
private Long currentTenantId() { |
|
|
|
|
|
|
|
return TokenData.currentTenantId(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* 归属校验(PRD FR-4.16):有 HTTP 请求上下文时,记录的 tenant_id / app_code 必须与当前请求一致, |
|
|
|
|
|
|
|
* 不匹配按“不存在”处理并返回 404,避免越权探测。 |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* <p>app_code 为可选维度:请求未携带 appCode 时不校验(FR-4.14)。</p> |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* <p>无请求上下文(MQ 消费、定时任务)不做校验,否则租户同步链路无法删除/更新租户数据。</p> |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @param rowTenantId 库中记录的 tenant_id。 |
|
|
|
|
|
|
|
* @param rowAppCode 库中记录的 app_code。 |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
private void verifyOwnership(Long rowTenantId, String rowAppCode) { |
|
|
|
|
|
|
|
TokenData tokenData = TokenData.takeFromRequest(); |
|
|
|
|
|
|
|
if (tokenData == null) { |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (!Objects.equals(rowTenantId, tokenData.getTenantId())) { |
|
|
|
|
|
|
|
throw new DataNotFoundException(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
String appCode = tokenData.getAppCode(); |
|
|
|
|
|
|
|
if (StringUtils.isNotBlank(appCode) && !Objects.equals(rowAppCode, appCode)) { |
|
|
|
|
|
|
|
throw new DataNotFoundException(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* 给 onl_form_head 查询追加租户隔离条件(PRD FR-4.15): |
|
|
|
|
|
|
|
* A 端 tenant_id = 当前租户;B 端 tenant_id IS NULL。app_code 有值才参与过滤(FR-4.14)。 |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
private void applyTenantScope(LambdaQueryWrapper<OnlFormHead> wrapper) { |
|
|
|
|
|
|
|
Long tenantId = currentTenantId(); |
|
|
|
|
|
|
|
if (tenantId == null) { |
|
|
|
|
|
|
|
wrapper.isNull(OnlFormHead::getTenantId); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
wrapper.eq(OnlFormHead::getTenantId, tenantId); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
String appCode = TokenData.currentAppCode(); |
|
|
|
|
|
|
|
if (StringUtils.isNotBlank(appCode)) { |
|
|
|
|
|
|
|
wrapper.eq(OnlFormHead::getAppCode, appCode); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
@Override |
|
|
|
public OnlFormHeadVo listByHeadId(Long id) { |
|
|
|
public OnlFormHeadVo listByHeadId(Long id) { |
|
|
|
@ -146,6 +197,15 @@ public class OnlFormHeadServiceImpl extends ServiceImpl<OnlFormHeadMapper, OnlFo |
|
|
|
@Transactional |
|
|
|
@Transactional |
|
|
|
public OnlFormHeadDto addAll(OnlFormHeadDto dto) { |
|
|
|
public OnlFormHeadDto addAll(OnlFormHeadDto dto) { |
|
|
|
OnlFormHead head = BeanUtil.copyProperties(dto, OnlFormHead.class); |
|
|
|
OnlFormHead head = BeanUtil.copyProperties(dto, OnlFormHead.class); |
|
|
|
|
|
|
|
//写接口强制赋值(PRD FR-4.17):A 端落当前租户,B 端保持 null。DTO 不暴露 tenantId,避免前端伪造。
|
|
|
|
|
|
|
|
Long tenantId = currentTenantId(); |
|
|
|
|
|
|
|
if (tenantId != null) { |
|
|
|
|
|
|
|
head.setTenantId(tenantId); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
String appCode = TokenData.currentAppCode(); |
|
|
|
|
|
|
|
if (StringUtils.isNotBlank(appCode)) { |
|
|
|
|
|
|
|
head.setAppCode(appCode); |
|
|
|
|
|
|
|
} |
|
|
|
String string = JSONUtil.parseObj(dto).toJSONString(0); |
|
|
|
String string = JSONUtil.parseObj(dto).toJSONString(0); |
|
|
|
head.setOldData(string); |
|
|
|
head.setOldData(string); |
|
|
|
//新增基本数据
|
|
|
|
//新增基本数据
|
|
|
|
@ -160,9 +220,10 @@ public class OnlFormHeadServiceImpl extends ServiceImpl<OnlFormHeadMapper, OnlFo |
|
|
|
//主表名
|
|
|
|
//主表名
|
|
|
|
String name = dto.getSort(); |
|
|
|
String name = dto.getSort(); |
|
|
|
if (StringUtils.isNotEmpty(name)) { |
|
|
|
if (StringUtils.isNotEmpty(name)) { |
|
|
|
List<OnlFormHead> list = this.list(new LambdaQueryWrapper<OnlFormHead>() |
|
|
|
LambdaQueryWrapper<OnlFormHead> mainWrapper = new LambdaQueryWrapper<OnlFormHead>() |
|
|
|
.eq(OnlFormHead::getTableName, name) |
|
|
|
.eq(OnlFormHead::getTableName, name); |
|
|
|
); |
|
|
|
applyTenantScope(mainWrapper); |
|
|
|
|
|
|
|
List<OnlFormHead> list = this.list(mainWrapper); |
|
|
|
if (ObjectUtil.isNotEmpty(list)) { |
|
|
|
if (ObjectUtil.isNotEmpty(list)) { |
|
|
|
for (OnlFormHead onlFormHead : list) { |
|
|
|
for (OnlFormHead onlFormHead : list) { |
|
|
|
String childName = onlFormHead.getChildName(); |
|
|
|
String childName = onlFormHead.getChildName(); |
|
|
|
@ -196,6 +257,18 @@ public class OnlFormHeadServiceImpl extends ServiceImpl<OnlFormHeadMapper, OnlFo |
|
|
|
@Override |
|
|
|
@Override |
|
|
|
@Transactional |
|
|
|
@Transactional |
|
|
|
public OnlFormHeadDto editAll(OnlFormHeadDto dto,boolean isync) { |
|
|
|
public OnlFormHeadDto editAll(OnlFormHeadDto dto,boolean isync) { |
|
|
|
|
|
|
|
//归属校验(PRD FR-4.16):必须在删缓存之前完成,避免跨租户请求清掉别人的缓存。
|
|
|
|
|
|
|
|
//isync=true 是 EAS 异构同步,数据由外部系统驱动,不做租户归属拦截。
|
|
|
|
|
|
|
|
if (dto.getId() == null) { |
|
|
|
|
|
|
|
throw new DataNotFoundException(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
OnlFormHead origin = this.getById(dto.getId()); |
|
|
|
|
|
|
|
if (origin == null) { |
|
|
|
|
|
|
|
throw new DataNotFoundException(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (!isync) { |
|
|
|
|
|
|
|
verifyOwnership(origin.getTenantId(), origin.getAppCode()); |
|
|
|
|
|
|
|
} |
|
|
|
//检查是否为附表,且有关联字段
|
|
|
|
//检查是否为附表,且有关联字段
|
|
|
|
String sortFiled = dto.getSortFiled(); |
|
|
|
String sortFiled = dto.getSortFiled(); |
|
|
|
String tableType = dto.getTableType(); |
|
|
|
String tableType = dto.getTableType(); |
|
|
|
@ -211,9 +284,10 @@ public class OnlFormHeadServiceImpl extends ServiceImpl<OnlFormHeadMapper, OnlFo |
|
|
|
formkey = CacheKey.makeOneToManyTable(name); |
|
|
|
formkey = CacheKey.makeOneToManyTable(name); |
|
|
|
redissonClient.getBucket(formkey).delete(); |
|
|
|
redissonClient.getBucket(formkey).delete(); |
|
|
|
if (StringUtils.isNotEmpty(name)) { |
|
|
|
if (StringUtils.isNotEmpty(name)) { |
|
|
|
OnlFormHead onlFormHead = this.getOne( |
|
|
|
LambdaQueryWrapper<OnlFormHead> mainWrapper = new LambdaQueryWrapper<OnlFormHead>() |
|
|
|
new LambdaQueryWrapper<OnlFormHead>().eq(OnlFormHead::getTableName, name) |
|
|
|
.eq(OnlFormHead::getTableName, name); |
|
|
|
); |
|
|
|
applyTenantScope(mainWrapper); |
|
|
|
|
|
|
|
OnlFormHead onlFormHead = this.getOne(mainWrapper); |
|
|
|
if (onlFormHead!=null) { |
|
|
|
if (onlFormHead!=null) { |
|
|
|
// BeanUtil.copyProperties(dto,onlFormHead);
|
|
|
|
// BeanUtil.copyProperties(dto,onlFormHead);
|
|
|
|
String childName = onlFormHead.getChildName(); |
|
|
|
String childName = onlFormHead.getChildName(); |
|
|
|
@ -236,6 +310,9 @@ public class OnlFormHeadServiceImpl extends ServiceImpl<OnlFormHeadMapper, OnlFo |
|
|
|
} |
|
|
|
} |
|
|
|
//数据转换
|
|
|
|
//数据转换
|
|
|
|
OnlFormHead head = BeanUtil.copyProperties(dto, OnlFormHead.class); |
|
|
|
OnlFormHead head = BeanUtil.copyProperties(dto, OnlFormHead.class); |
|
|
|
|
|
|
|
//归属以库中记录为准,不信任提交内容,防止 updateById 把 tenant_id 刷成 NULL(PRD FR-4.17 / FR-4.16)
|
|
|
|
|
|
|
|
head.setTenantId(origin.getTenantId()); |
|
|
|
|
|
|
|
head.setAppCode(origin.getAppCode()); |
|
|
|
head.setVersion(String.valueOf(Integer.parseInt(head.getVersion()) + 1)); |
|
|
|
head.setVersion(String.valueOf(Integer.parseInt(head.getVersion()) + 1)); |
|
|
|
head.setStatus("0"); |
|
|
|
head.setStatus("0"); |
|
|
|
//更新基本数据
|
|
|
|
//更新基本数据
|
|
|
|
@ -252,10 +329,22 @@ public class OnlFormHeadServiceImpl extends ServiceImpl<OnlFormHeadMapper, OnlFo |
|
|
|
dto.setId(head.getId()); |
|
|
|
dto.setId(head.getId()); |
|
|
|
//更新分录数据
|
|
|
|
//更新分录数据
|
|
|
|
if (save && ObjectUtil.isNotEmpty(head.getId())) { |
|
|
|
if (save && ObjectUtil.isNotEmpty(head.getId())) { |
|
|
|
//先删除
|
|
|
|
// 判定口径(PRD FR-4.13):tenant_id 有值 = 租户端(A),为空 = 管理端(B)。
|
|
|
|
removeChildData(head.getId(),isync); |
|
|
|
// 用库中记录判断,DTO 没有 tenantId 字段,不能拿 DTO 判断。
|
|
|
|
//再添加
|
|
|
|
// isync=true 是 EAS 异构同步,外部系统为权威源,同样保持“先清理再重插”。
|
|
|
|
addChildData(dto); |
|
|
|
if (isync || origin.getTenantId() == null) { |
|
|
|
|
|
|
|
// B 端设计器提交的是完整字段集,删除字段/改字段名必须靠“先删后插”才能落库;
|
|
|
|
|
|
|
|
// 这里也是 removeChildData 在保存路径上的唯一保留点。
|
|
|
|
|
|
|
|
removeChildData(head.getId(), isync); |
|
|
|
|
|
|
|
addChildData(dto); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
// A 端保存:改为增量 upsert(PRD FR-4.9 / 任务 #5)。
|
|
|
|
|
|
|
|
// 原实现是“按 headId 整表删除 + 按提交内容重插”,租户端只提交部分字段时,
|
|
|
|
|
|
|
|
// 管理端下发但本次未提交的字段会被物理删除(P-9)。
|
|
|
|
|
|
|
|
// 注意:A 端的 tenantId 由任务 #6 在进入本方法前强制赋值(FR-4.17)。
|
|
|
|
|
|
|
|
upsertChildData(head.getId(), dto); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
// TODO(FR-4.9): A 端删除自有 TENANT 字段本期不做(N-6 只能隐藏),后续按 source='TENANT' 补删
|
|
|
|
} |
|
|
|
} |
|
|
|
return dto; |
|
|
|
return dto; |
|
|
|
} |
|
|
|
} |
|
|
|
@ -263,6 +352,12 @@ public class OnlFormHeadServiceImpl extends ServiceImpl<OnlFormHeadMapper, OnlFo |
|
|
|
@Override |
|
|
|
@Override |
|
|
|
@Transactional |
|
|
|
@Transactional |
|
|
|
public Boolean removeAllById(Long id) { |
|
|
|
public Boolean removeAllById(Long id) { |
|
|
|
|
|
|
|
//归属校验后再删除(PRD FR-4.16 / FR-4.12),避免传别家 id 直接删库
|
|
|
|
|
|
|
|
OnlFormHead origin = id == null ? null : this.getById(id); |
|
|
|
|
|
|
|
if (origin == null) { |
|
|
|
|
|
|
|
throw new DataNotFoundException(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
verifyOwnership(origin.getTenantId(), origin.getAppCode()); |
|
|
|
//删除主数据
|
|
|
|
//删除主数据
|
|
|
|
boolean remove = this.removeById(id); |
|
|
|
boolean remove = this.removeById(id); |
|
|
|
if (remove) { |
|
|
|
if (remove) { |
|
|
|
@ -920,7 +1015,11 @@ public class OnlFormHeadServiceImpl extends ServiceImpl<OnlFormHeadMapper, OnlFo |
|
|
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
@Override |
|
|
|
public Boolean checkTableName(String tableName) { |
|
|
|
public Boolean checkTableName(String tableName) { |
|
|
|
List<OnlFormHead> list = this.list(new LambdaQueryWrapper<OnlFormHead>().eq(OnlFormHead::getTableName, tableName)); |
|
|
|
//同一租户内判重,避免跨租户误判表名已存在(PRD FR-4.15)
|
|
|
|
|
|
|
|
LambdaQueryWrapper<OnlFormHead> wrapper = new LambdaQueryWrapper<OnlFormHead>() |
|
|
|
|
|
|
|
.eq(OnlFormHead::getTableName, tableName); |
|
|
|
|
|
|
|
applyTenantScope(wrapper); |
|
|
|
|
|
|
|
List<OnlFormHead> list = this.list(wrapper); |
|
|
|
return list.isEmpty(); |
|
|
|
return list.isEmpty(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -928,7 +1027,11 @@ public class OnlFormHeadServiceImpl extends ServiceImpl<OnlFormHeadMapper, OnlFo |
|
|
|
public JSONObject tableInfo(Long id) { |
|
|
|
public JSONObject tableInfo(Long id) { |
|
|
|
JSONObject result = new JSONObject(); |
|
|
|
JSONObject result = new JSONObject(); |
|
|
|
//主表
|
|
|
|
//主表
|
|
|
|
OnlFormHead main = this.getById(id); |
|
|
|
OnlFormHead main = id == null ? null : this.getById(id); |
|
|
|
|
|
|
|
if (main == null) { |
|
|
|
|
|
|
|
throw new DataNotFoundException(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
verifyOwnership(main.getTenantId(), main.getAppCode()); |
|
|
|
result.put("main", main); |
|
|
|
result.put("main", main); |
|
|
|
String tableType = main.getTableType(); |
|
|
|
String tableType = main.getTableType(); |
|
|
|
//风格
|
|
|
|
//风格
|
|
|
|
@ -959,9 +1062,10 @@ public class OnlFormHeadServiceImpl extends ServiceImpl<OnlFormHeadMapper, OnlFo |
|
|
|
vo3.setNote("Tab风格"); |
|
|
|
vo3.setNote("Tab风格"); |
|
|
|
list.add(vo3); |
|
|
|
list.add(vo3); |
|
|
|
//附表
|
|
|
|
//附表
|
|
|
|
List<OnlFormHead> sub = this.list(new LambdaQueryWrapper<OnlFormHead>() |
|
|
|
LambdaQueryWrapper<OnlFormHead> subWrapper = new LambdaQueryWrapper<OnlFormHead>() |
|
|
|
.in(OnlFormHead::getTableName, main.getChildName().split(",")) |
|
|
|
.in(OnlFormHead::getTableName, main.getChildName().split(",")); |
|
|
|
); |
|
|
|
applyTenantScope(subWrapper); |
|
|
|
|
|
|
|
List<OnlFormHead> sub = this.list(subWrapper); |
|
|
|
if (ObjectUtil.isNotEmpty(sub)) { |
|
|
|
if (ObjectUtil.isNotEmpty(sub)) { |
|
|
|
result.put("sub", sub); |
|
|
|
result.put("sub", sub); |
|
|
|
List<OnlFormCodeDto> subList = new ArrayList<>(); |
|
|
|
List<OnlFormCodeDto> subList = new ArrayList<>(); |
|
|
|
@ -999,7 +1103,123 @@ public class OnlFormHeadServiceImpl extends ServiceImpl<OnlFormHeadMapper, OnlFo |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//删除子数据
|
|
|
|
/** |
|
|
|
|
|
|
|
* 子数据增量写入:只 upsert,不做物理删除(PRD FR-4.9 / 任务 #5)。 |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* <p>规则与任务清单里的“已确认的关键决策”一致:</p> |
|
|
|
|
|
|
|
* <ul> |
|
|
|
|
|
|
|
* <li>提交里有、库里没有 → 插入;</li> |
|
|
|
|
|
|
|
* <li>提交里有、库里也有 → 更新;</li> |
|
|
|
|
|
|
|
* <li>库里有、提交里没有 → 保留(不删)。这是本方法存在的全部意义:租户端只提交自己关心的字段时, |
|
|
|
|
|
|
|
* 管理端下发但本次未提交的字段不会被物理删除(P-9 / R-2)。</li> |
|
|
|
|
|
|
|
* </ul> |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* <p>为什么还需要回填 id:旧实现靠前面的 {@code removeChildData} 全删来保证“重插不产生重复行”。 |
|
|
|
|
|
|
|
* 去掉删除以后,若前端不传子行 id(新增行本来就没有 id),同一行重复提交会插出两行同名字段。 |
|
|
|
|
|
|
|
* 因此提交行没带 id(或带的 id 不属于当前 headId)时,按业务唯一键回填库里已有行的 id 后再走更新。</p> |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* <p>子表语义说明:六张子表都只通过 {@code head_id} 从属父表、没有独立的删除入口(N-6 本期不做删除, |
|
|
|
|
|
|
|
* A 端只能隐藏),所以统一按“不删”处理,不区分是哪张子表。</p> |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @param headId 父表 id,子行的 headId 会在 {@link #addChildData} 里被强制改写为该值。 |
|
|
|
|
|
|
|
* @param dto 本次提交的表单数据,方法会就地回填子行 id 并覆盖其子表集合。 |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
private void upsertChildData(Long headId, OnlFormHeadDto dto) { |
|
|
|
|
|
|
|
dto.setOnlFormFieldList(this.mergeChildIds(dto.getOnlFormFieldList(), |
|
|
|
|
|
|
|
onlFormFieldService.list(new LambdaQueryWrapper<OnlFormField>().eq(OnlFormField::getHeadId, headId)), |
|
|
|
|
|
|
|
OnlFormField::getId, OnlFormField::setId, OnlFormField::getFieldName)); |
|
|
|
|
|
|
|
dto.setOnlFormFindList(this.mergeChildIds(dto.getOnlFormFindList(), |
|
|
|
|
|
|
|
onlFormFindService.list(new LambdaQueryWrapper<OnlFormFind>().eq(OnlFormFind::getHeadId, headId)), |
|
|
|
|
|
|
|
OnlFormFind::getId, OnlFormFind::setId, OnlFormFind::getFieldName)); |
|
|
|
|
|
|
|
dto.setOnlFormCheckList(this.mergeChildIds(dto.getOnlFormCheckList(), |
|
|
|
|
|
|
|
onlFormCheckService.list(new LambdaQueryWrapper<OnlFormCheck>().eq(OnlFormCheck::getHeadId, headId)), |
|
|
|
|
|
|
|
OnlFormCheck::getId, OnlFormCheck::setId, OnlFormCheck::getFieldName)); |
|
|
|
|
|
|
|
dto.setOnlFormIndexList(this.mergeChildIds(dto.getOnlFormIndexList(), |
|
|
|
|
|
|
|
onlFormIndexService.list(new LambdaQueryWrapper<OnlFormIndex>().eq(OnlFormIndex::getHeadId, headId)), |
|
|
|
|
|
|
|
OnlFormIndex::getId, OnlFormIndex::setId, OnlFormIndex::getIndexName)); |
|
|
|
|
|
|
|
dto.setOnlFormKeyList(this.mergeChildIds(dto.getOnlFormKeyList(), |
|
|
|
|
|
|
|
onlFormKeyService.list(new LambdaQueryWrapper<OnlFormKey>().eq(OnlFormKey::getHeadId, headId)), |
|
|
|
|
|
|
|
OnlFormKey::getId, OnlFormKey::setId, OnlFormKey::getFieldName)); |
|
|
|
|
|
|
|
dto.setOnlFormPageList(this.mergeChildIds(dto.getOnlFormPageList(), |
|
|
|
|
|
|
|
onlFormPageService.list(new LambdaQueryWrapper<OnlFormPage>().eq(OnlFormPage::getHeadId, headId)), |
|
|
|
|
|
|
|
OnlFormPage::getId, OnlFormPage::setId, OnlFormPage::getFieldName)); |
|
|
|
|
|
|
|
// addChildData 内部用 saveOrUpdateBatch:id 有值走更新、无值走插入,正好匹配上面的回填结果。
|
|
|
|
|
|
|
|
addChildData(dto); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* 为提交的子行决定最终 id,并按 id / 业务唯一键去重(同一行重复提交只保留最后一条)。 |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* <p>三种情况:</p> |
|
|
|
|
|
|
|
* <ol> |
|
|
|
|
|
|
|
* <li>提交行 id 已存在于当前 headId 下 → 保持 id,更新该行;</li> |
|
|
|
|
|
|
|
* <li>提交行 id 为空、或该 id 不属于当前 headId(可能是伪造/串种)→ 先按业务唯一键找库里同名的行, |
|
|
|
|
|
|
|
* 找到就用它的 id(更新),找不到就置空 id(插入)。<b>不直接用提交进来的 id 落库</b>, |
|
|
|
|
|
|
|
* 否则可以把别的父表下的子行“搬”到当前父表(对照 FR-4.16 的归属校验)。</li> |
|
|
|
|
|
|
|
* <li>同一次提交里出现两条同一业务唯一键 → 后者覆盖前者。</li> |
|
|
|
|
|
|
|
* </ol> |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @param submitted 本次提交的子行集合,允许为 null/空(返回 null,调用方不会删任何数据)。 |
|
|
|
|
|
|
|
* @param existing 库里当前 headId 下已有的子行。 |
|
|
|
|
|
|
|
* @param idGetter 子行 id 读取器。 |
|
|
|
|
|
|
|
* @param idSetter 子行 id 写回器(就地回填)。 |
|
|
|
|
|
|
|
* @param keyGetter 业务唯一键读取器(字段名 / 索引名)。 |
|
|
|
|
|
|
|
* @return 回填 id 并去重后的提交集合。 |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
private <T> List<T> mergeChildIds(List<T> submitted, List<T> existing, |
|
|
|
|
|
|
|
Function<T, Long> idGetter, BiConsumer<T, Long> idSetter, |
|
|
|
|
|
|
|
Function<T, String> keyGetter) { |
|
|
|
|
|
|
|
if (ObjectUtil.isEmpty(submitted)) { |
|
|
|
|
|
|
|
return submitted; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
Set<Long> existingIds = new HashSet<>(); |
|
|
|
|
|
|
|
Map<String, Long> keyToId = new HashMap<>(); |
|
|
|
|
|
|
|
if (ObjectUtil.isNotEmpty(existing)) { |
|
|
|
|
|
|
|
for (T row : existing) { |
|
|
|
|
|
|
|
Long id = idGetter.apply(row); |
|
|
|
|
|
|
|
if (id == null) { |
|
|
|
|
|
|
|
continue; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
existingIds.add(id); |
|
|
|
|
|
|
|
String key = this.normalizeChildKey(keyGetter.apply(row)); |
|
|
|
|
|
|
|
if (key != null && !keyToId.containsKey(key)) { |
|
|
|
|
|
|
|
keyToId.put(key, id); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
Map<String, T> merged = new LinkedHashMap<>(); |
|
|
|
|
|
|
|
int newRowIndex = 0; |
|
|
|
|
|
|
|
for (T row : submitted) { |
|
|
|
|
|
|
|
Long id = idGetter.apply(row); |
|
|
|
|
|
|
|
String key = this.normalizeChildKey(keyGetter.apply(row)); |
|
|
|
|
|
|
|
if (id != null && existingIds.contains(id)) { |
|
|
|
|
|
|
|
merged.put("id:" + id, row); |
|
|
|
|
|
|
|
continue; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (key != null && keyToId.containsKey(key)) { |
|
|
|
|
|
|
|
idSetter.accept(row, keyToId.get(key)); |
|
|
|
|
|
|
|
merged.put("key:" + key, row); |
|
|
|
|
|
|
|
continue; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (id != null) { |
|
|
|
|
|
|
|
log.warn("子表数据提交了不属于当前元数据头的id,已按新增处理。id={}", id); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
idSetter.accept(row, null); |
|
|
|
|
|
|
|
merged.put(key != null ? "key:" + key : "new:" + (newRowIndex++), row); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return new ArrayList<>(merged.values()); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* 子行业务唯一键归一化:去空格、转小写,空串视为无键(不参与同名匹配)。 |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
private String normalizeChildKey(String key) { |
|
|
|
|
|
|
|
String normalized = StringUtils.trimToNull(key); |
|
|
|
|
|
|
|
return normalized == null ? null : normalized.toLowerCase(Locale.ROOT); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//删除子数据。removeAllById(整表物理删除)与 EAS 异构同步(isync=true,外部系统为准)仍走这里;
|
|
|
|
|
|
|
|
//表单设计器保存已改为 upsertChildData,不再调用本方法。
|
|
|
|
private <T> void removeChildData(T id,boolean isync) { |
|
|
|
private <T> void removeChildData(T id,boolean isync) { |
|
|
|
onlFormFindService.remove(new LambdaQueryWrapper<OnlFormFind>() |
|
|
|
onlFormFindService.remove(new LambdaQueryWrapper<OnlFormFind>() |
|
|
|
.eq(OnlFormFind::getHeadId, id) |
|
|
|
.eq(OnlFormFind::getHeadId, id) |
|
|
|
@ -1023,7 +1243,12 @@ public class OnlFormHeadServiceImpl extends ServiceImpl<OnlFormHeadMapper, OnlFo |
|
|
|
|
|
|
|
|
|
|
|
//获取子数据vo
|
|
|
|
//获取子数据vo
|
|
|
|
private void getChildData(OnlFormHeadVo vo, Long id) { |
|
|
|
private void getChildData(OnlFormHeadVo vo, Long id) { |
|
|
|
OnlFormHead main = this.getById(id); |
|
|
|
OnlFormHead main = id == null ? null : this.getById(id); |
|
|
|
|
|
|
|
if (main == null) { |
|
|
|
|
|
|
|
throw new DataNotFoundException(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
//按 id 查询时必须校验归属(PRD FR-4.16),子表通过 head_id 随之隔离
|
|
|
|
|
|
|
|
verifyOwnership(main.getTenantId(), main.getAppCode()); |
|
|
|
BeanUtil.copyProperties(main, vo); |
|
|
|
BeanUtil.copyProperties(main, vo); |
|
|
|
vo.setOnlFormFindList(onlFormFindService.list(new LambdaQueryWrapper<OnlFormFind>().eq(OnlFormFind::getHeadId, id))); |
|
|
|
vo.setOnlFormFindList(onlFormFindService.list(new LambdaQueryWrapper<OnlFormFind>().eq(OnlFormFind::getHeadId, id))); |
|
|
|
vo.setOnlFormFieldList(onlFormFieldService.queryOnlFormField(new LambdaQueryWrapper<OnlFormField>().eq(OnlFormField::getHeadId, id))); |
|
|
|
vo.setOnlFormFieldList(onlFormFieldService.queryOnlFormField(new LambdaQueryWrapper<OnlFormField>().eq(OnlFormField::getHeadId, id))); |
|
|
|
|