diff --git a/common/common-generator/src/main/java/apelet/common/generator/controller/OnlCgreportHeadController.java b/common/common-generator/src/main/java/apelet/common/generator/controller/OnlCgreportHeadController.java
index dbf518b..7ab8a04 100644
--- a/common/common-generator/src/main/java/apelet/common/generator/controller/OnlCgreportHeadController.java
+++ b/common/common-generator/src/main/java/apelet/common/generator/controller/OnlCgreportHeadController.java
@@ -6,6 +6,7 @@ import apelet.common.core.object.MyOrderParam;
import apelet.common.core.object.MyPageData;
import apelet.common.core.object.MyPageParam;
import apelet.common.core.object.ResponseResult;
+import apelet.common.core.object.TokenData;
import apelet.common.core.util.MyPageUtil;
import apelet.common.generator.dto.OnlCgreportHeadDto;
import apelet.common.generator.dto.SQLParseDto;
@@ -137,6 +138,17 @@ public class OnlCgreportHeadController {
wq.like(StringUtils.isNotEmpty(OnlCgreportHeadDto.getCode()),OnlCgreportHead::getCode,OnlCgreportHeadDto.getCode())
.like(StringUtils.isNotEmpty(OnlCgreportHeadDto.getName()),OnlCgreportHead::getName,OnlCgreportHeadDto.getName());
}
+ //租户隔离(PRD FR-4.15):A 端只看本租户,B 端只看 tenant_id IS NULL
+ Long tenantId = TokenData.currentTenantId();
+ if (tenantId == null) {
+ wq.isNull(OnlCgreportHead::getTenantId);
+ } else {
+ wq.eq(OnlCgreportHead::getTenantId, tenantId);
+ }
+ String appCode = TokenData.currentAppCode();
+ if (StringUtils.isNotBlank(appCode)) {
+ wq.eq(OnlCgreportHead::getAppCode, appCode);
+ }
List list = onlCgreportHeadService.list(wq);
long totalCount = 0L;
if (list instanceof com.github.pagehelper.Page) {
@@ -152,7 +164,8 @@ public class OnlCgreportHeadController {
*/
@GetMapping(value = "/getById")
public ResponseResult> getById(@RequestParam("id") String id) {
- return ResponseResult.success(onlCgreportHeadService.getById(id));
+ //归属校验:不存在或不属于当前租户返回 404(PRD FR-4.16)
+ return ResponseResult.success(onlCgreportHeadService.getOwnedHead(id));
}
/**
* 查询对应sql的字段
diff --git a/common/common-generator/src/main/java/apelet/common/generator/controller/OnlFormHeadController.java b/common/common-generator/src/main/java/apelet/common/generator/controller/OnlFormHeadController.java
index 782cc84..4a2013f 100644
--- a/common/common-generator/src/main/java/apelet/common/generator/controller/OnlFormHeadController.java
+++ b/common/common-generator/src/main/java/apelet/common/generator/controller/OnlFormHeadController.java
@@ -7,6 +7,7 @@ import apelet.common.core.object.MyOrderParam;
import apelet.common.core.object.MyPageData;
import apelet.common.core.object.MyPageParam;
import apelet.common.core.object.ResponseResult;
+import apelet.common.core.object.TokenData;
import apelet.common.core.util.MyPageUtil;
import apelet.common.eas.service.impl.EASSyncService;
import apelet.common.generator.dto.OnlFormCodeDto;
@@ -101,6 +102,18 @@ public class OnlFormHeadController {
.like(ObjectUtil.isNotEmpty(dto.getTableType()), OnlFormHead::getTableType, dto.getTableType())
.like(ObjectUtil.isNotEmpty(dto.getTableTxt()), OnlFormHead::getTableTxt, dto.getTableTxt());
}
+ //租户隔离:A 端只看本租户,B 端只看 tenant_id IS NULL 的管理端数据
+ Long tenantId = TokenData.currentTenantId();
+ if (tenantId == null) {
+ wq.isNull(OnlFormHead::getTenantId);
+ } else {
+ wq.eq(OnlFormHead::getTenantId, tenantId);
+ }
+ //app_code 为可选维度:有值才过滤(PRD FR-4.14)
+ String appCode = TokenData.currentAppCode();
+ if (StringUtils.isNotBlank(appCode)) {
+ wq.eq(OnlFormHead::getAppCode, appCode);
+ }
wq.select(OnlFormHead.class, i -> !i.getProperty().equals("oldData"));
List list = onlFormHeadService.list(wq);
long totalCount = 0L;
@@ -387,7 +400,7 @@ public class OnlFormHeadController {
* @return
*/
@GetMapping(value = "/onlFormSync")
- ResponseResult> onlFormSync(@RequestParam("billType") String billType, @RequestParam("id") Long id) {
+ public ResponseResult> onlFormSync(@RequestParam("billType") String billType, @RequestParam("id") Long id) {
try {
if (StringUtils.isEmpty(billType)) {
return ResponseResult.error("500", "标识为空 ");
diff --git a/common/common-generator/src/main/java/apelet/common/generator/model/OnlCgreportHead.java b/common/common-generator/src/main/java/apelet/common/generator/model/OnlCgreportHead.java
index ec3280d..4ff74be 100644
--- a/common/common-generator/src/main/java/apelet/common/generator/model/OnlCgreportHead.java
+++ b/common/common-generator/src/main/java/apelet/common/generator/model/OnlCgreportHead.java
@@ -55,4 +55,8 @@ public class OnlCgreportHead extends BaseModel implements Serializable {
@Schema(description = "租户id")
@TableField(value = "tenant_id")
private Long tenantId;
+
+ @Schema(description = "应用编码")
+ @TableField(value = "app_code")
+ private String appCode;
}
diff --git a/common/common-generator/src/main/java/apelet/common/generator/model/OnlFormHead.java b/common/common-generator/src/main/java/apelet/common/generator/model/OnlFormHead.java
index b155e65..073a140 100644
--- a/common/common-generator/src/main/java/apelet/common/generator/model/OnlFormHead.java
+++ b/common/common-generator/src/main/java/apelet/common/generator/model/OnlFormHead.java
@@ -118,6 +118,10 @@ public class OnlFormHead extends BaseModel implements Serializable {
@TableField(value = "tenant_id")
private Long tenantId;
+ @Schema(description = "应用编码")
+ @TableField(value = "app_code")
+ private String appCode;
+
@Schema(description = "数据库属性分录")
@TableField(exist = false)
private List onlFormFieldList;
diff --git a/common/common-generator/src/main/java/apelet/common/generator/service/IOnlCgreportHeadService.java b/common/common-generator/src/main/java/apelet/common/generator/service/IOnlCgreportHeadService.java
index b9e409c..420b8b0 100644
--- a/common/common-generator/src/main/java/apelet/common/generator/service/IOnlCgreportHeadService.java
+++ b/common/common-generator/src/main/java/apelet/common/generator/service/IOnlCgreportHeadService.java
@@ -24,6 +24,11 @@ import java.util.Map;
public interface IOnlCgreportHeadService extends IService {
Map getSql(String sql, String dbKey);
+ /**
+ * 按 id 查询并校验租户归属,不存在或不属于当前租户时抛 404。
+ */
+ OnlCgreportHead getOwnedHead(String id);
+
List getFieldsById(String id);
List getParamsById(String id);
diff --git a/common/common-generator/src/main/java/apelet/common/generator/service/impl/OnlCgreportHeadServiceImpl.java b/common/common-generator/src/main/java/apelet/common/generator/service/impl/OnlCgreportHeadServiceImpl.java
index 4a5ba1c..17a33fd 100644
--- a/common/common-generator/src/main/java/apelet/common/generator/service/impl/OnlCgreportHeadServiceImpl.java
+++ b/common/common-generator/src/main/java/apelet/common/generator/service/impl/OnlCgreportHeadServiceImpl.java
@@ -1,5 +1,6 @@
package apelet.common.generator.service.impl;
+import apelet.common.core.exception.DataNotFoundException;
import apelet.common.core.object.MyPageParam;
import apelet.common.core.object.TokenData;
import apelet.common.dbutil.util.SQLStringBuffer;
@@ -66,6 +67,49 @@ public class OnlCgreportHeadServiceImpl extends ServiceImpl wrapper) {
+ Long tenantId = TokenData.currentTenantId();
+ if (tenantId == null) {
+ wrapper.isNull(OnlCgreportHead::getTenantId);
+ } else {
+ wrapper.eq(OnlCgreportHead::getTenantId, tenantId);
+ }
+ String appCode = TokenData.currentAppCode();
+ if (StringUtils.isNotBlank(appCode)) {
+ wrapper.eq(OnlCgreportHead::getAppCode, appCode);
+ }
+ }
+
+ /**
+ * 按 id 查询并校验归属:不存在或不属于当前租户统一抛 404。
+ *
+ * 无 HTTP 请求上下文(MQ、定时任务)不做租户校验,避免阻断同步链路。
+ */
+ @Override
+ public OnlCgreportHead getOwnedHead(String id) {
+ OnlCgreportHead head = StringUtils.isEmpty(id) ? null : this.getById(id);
+ if (head == null) {
+ throw new DataNotFoundException();
+ }
+ TokenData tokenData = TokenData.takeFromRequest();
+ if (tokenData == null) {
+ return head;
+ }
+ if (!Objects.equals(head.getTenantId(), tokenData.getTenantId())) {
+ throw new DataNotFoundException();
+ }
+ //app_code 为可选维度:请求未携带 appCode 时不校验(FR-4.14)
+ String appCode = tokenData.getAppCode();
+ if (StringUtils.isNotBlank(appCode) && !Objects.equals(head.getAppCode(), appCode)) {
+ throw new DataNotFoundException();
+ }
+ return head;
+ }
+
@Override
public Map getSql(String sql, String dbKey) {
//检查sql是否有数据
@@ -191,6 +235,8 @@ public class OnlCgreportHeadServiceImpl extends ServiceImpl();
}
+ //先校验父表归属,子表只通过 cgrhead_id 关联(子表不加 tenant_id)
+ getOwnedHead(id);
LambdaQueryWrapper wq = new LambdaQueryWrapper()
.eq(StringUtils.isNotEmpty(id), OnlCgreportItem::getCgrheadId, id);
return onlCgreportItemService.list(wq);
@@ -201,6 +247,7 @@ public class OnlCgreportHeadServiceImpl extends ServiceImpl();
}
+ getOwnedHead(id);
LambdaQueryWrapper wq = new LambdaQueryWrapper()
.eq(StringUtils.isNotEmpty(id), OnlCgreportParam::getCgrheadId, id);
return onlCgreportParamService.list(wq);
@@ -210,17 +257,33 @@ public class OnlCgreportHeadServiceImpl extends ServiceImpl items = dto.getItems();
List params = dto.getParams();
if (StringUtils.isNotEmpty(id)) {
+ //新增报表时子表行一定是新行:清掉前端传来的 id,避免复用他行主键导致主键冲突(任务 #8)
if (ObjectUtil.isNotEmpty(items)) {
- List itemList = items.parallelStream().peek(x -> x.setCgrheadId(id)).collect(Collectors.toList());
+ List itemList = items.stream().peek(x -> {
+ x.setId(null);
+ x.setCgrheadId(id);
+ }).collect(Collectors.toList());
onlCgreportItemService.saveBatch(itemList);
}
if (ObjectUtil.isNotEmpty(params)) {
- List paramList = params.parallelStream().peek(x -> x.setCgrheadId(id)).collect(Collectors.toList());
+ List paramList = params.stream().peek(x -> {
+ x.setId(null);
+ x.setCgrheadId(id);
+ }).collect(Collectors.toList());
onlCgreportParamService.saveBatch(paramList);
}
return id;
@@ -231,31 +294,137 @@ public class OnlCgreportHeadServiceImpl extends ServiceImpl items = dto.getItems();
- List params = dto.getParams();
- //查询原数据没改动就不更新
- if (StringUtils.isNotEmpty(id)) {
- //删除原有数据
- onlCgreportItemService.remove(new LambdaQueryWrapper().eq(OnlCgreportItem::getCgrheadId, id));
- onlCgreportParamService.remove(new LambdaQueryWrapper().eq(OnlCgreportParam::getCgrheadId, id));
- //重新添加
- if (ObjectUtil.isNotEmpty(params)) {
- onlCgreportParamService.saveOrUpdateBatch(params.stream().peek(x -> x.setCgrheadId(id)).collect(Collectors.toList()));
+ if (StringUtils.isEmpty(id)) {
+ return "修改成功";
+ }
+ //子表改为增量 upsert(任务 #8):不再无条件“先全删再重插”。
+ // 1) items/params 为 null 表示本次没有提交该子表,库中配置保持不变;
+ // 2) 管理端(tenant_id 为空)提交的是完整配置,删除本次未提交的行,保证删字段/删参数能落库;
+ // 3) 租户端(tenant_id 有值)只 upsert 不做物理删除,与任务 #5 的口径一致。
+ boolean admin = origin.getTenantId() == null;
+ saveReportItems(id, dto.getItems(), admin);
+ saveReportParams(id, dto.getParams(), admin);
+ return id;
+ }
+
+ /**
+ * 增量保存报表字段(任务 #8)。
+ *
+ * @param headId 报表主键Id。
+ * @param items 本次提交的字段,null 表示本次未提交。
+ * @param admin 是否管理端;管理端以提交内容为准,会删除本次未提交的行。
+ */
+ private void saveReportItems(String headId, List items, boolean admin) {
+ if (items == null) {
+ return;
+ }
+ Map existing = this.listReportItems(headId);
+ List toSave = new ArrayList<>(items.size());
+ Set submittedIds = new HashSet<>();
+ for (OnlCgreportItem item : items) {
+ //只允许更新本报表下的行,伪造或来自其它报表的 id 一律按新增处理,避免把他行数据挂到当前报表
+ if (StringUtils.isEmpty(item.getId()) || !existing.containsKey(item.getId())) {
+ item.setId(null);
+ } else {
+ submittedIds.add(item.getId());
}
- if (ObjectUtil.isNotEmpty(items)) {
- onlCgreportItemService.saveOrUpdateBatch(items.stream().peek(x -> x.setCgrheadId(id)).collect(Collectors.toList()));
+ item.setCgrheadId(headId);
+ toSave.add(item);
+ }
+ if (admin) {
+ List removeIds = existing.keySet().stream()
+ .filter(existingId -> !submittedIds.contains(existingId))
+ .collect(Collectors.toList());
+ if (!removeIds.isEmpty()) {
+ onlCgreportItemService.removeByIds(removeIds);
}
- return id;
}
- return "修改成功";
+ if (!toSave.isEmpty()) {
+ onlCgreportItemService.saveOrUpdateBatch(toSave);
+ }
+ }
+
+ /**
+ * 增量保存报表参数(任务 #8),口径同 {@link #saveReportItems(String, List, boolean)}。
+ *
+ * @param headId 报表主键Id。
+ * @param params 本次提交的参数,null 表示本次未提交。
+ * @param admin 是否管理端;管理端以提交内容为准,会删除本次未提交的行。
+ */
+ private void saveReportParams(String headId, List params, boolean admin) {
+ if (params == null) {
+ return;
+ }
+ Map existing = this.listReportParams(headId);
+ List toSave = new ArrayList<>(params.size());
+ Set submittedIds = new HashSet<>();
+ for (OnlCgreportParam param : params) {
+ if (StringUtils.isEmpty(param.getId()) || !existing.containsKey(param.getId())) {
+ param.setId(null);
+ } else {
+ submittedIds.add(param.getId());
+ }
+ param.setCgrheadId(headId);
+ toSave.add(param);
+ }
+ if (admin) {
+ List removeIds = existing.keySet().stream()
+ .filter(existingId -> !submittedIds.contains(existingId))
+ .collect(Collectors.toList());
+ if (!removeIds.isEmpty()) {
+ onlCgreportParamService.removeByIds(removeIds);
+ }
+ }
+ if (!toSave.isEmpty()) {
+ onlCgreportParamService.saveOrUpdateBatch(toSave);
+ }
+ }
+
+ /**
+ * 查询报表下的全部字段,按主键索引。
+ *
+ * @param headId 报表主键Id。
+ * @return 主键到字段对象的映射。
+ */
+ private Map listReportItems(String headId) {
+ List list = onlCgreportItemService.list(
+ new LambdaQueryWrapper().eq(OnlCgreportItem::getCgrheadId, headId));
+ Map result = new HashMap<>(list.size());
+ for (OnlCgreportItem item : list) {
+ result.put(item.getId(), item);
+ }
+ return result;
+ }
+
+ /**
+ * 查询报表下的全部参数,按主键索引。
+ *
+ * @param headId 报表主键Id。
+ * @return 主键到参数对象的映射。
+ */
+ private Map listReportParams(String headId) {
+ List list = onlCgreportParamService.list(
+ new LambdaQueryWrapper().eq(OnlCgreportParam::getCgrheadId, headId));
+ Map result = new HashMap<>(list.size());
+ for (OnlCgreportParam param : list) {
+ result.put(param.getId(), param);
+ }
+ return result;
}
@Override
@Transactional
public String removeAllById(Long id) {
+ //先校验归属再删(PRD FR-4.12 / FR-4.16)
+ getOwnedHead(id == null ? null : String.valueOf(id));
String msg = "删除数据成功";
baseMapper.deleteById(id);
onlCgreportItemService.remove(new LambdaQueryWrapper()
@@ -270,10 +439,13 @@ public class OnlCgreportHeadServiceImpl extends ServiceImpl list = this.list(new LambdaQueryWrapper()
- .eq(OnlCgreportHead::getCode, code)
- );
- if (ObjectUtil.isEmpty(list) || list.size() > 1) throw new RuntimeException("code编码 配置多条数据");
+ //同一租户内按 code 定位,避免多租户同名 code 冲突(同步端允许 code 在不同租户重复)
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper()
+ .eq(OnlCgreportHead::getCode, code);
+ applyTenantScope(wrapper);
+ List list = this.list(wrapper);
+ if (ObjectUtil.isEmpty(list)) throw new DataNotFoundException();
+ if (list.size() > 1) throw new RuntimeException("code编码 配置多条数据");
//返回参数
OnlFormHeadVo2 result = new OnlFormHeadVo2();
OnlCgreportHead head = list.get(0);
@@ -389,7 +561,7 @@ public class OnlCgreportHeadServiceImpl extends ServiceImpl getData(JSONObject jsonObject, String id, MyPageParam pageParam) {
- OnlCgreportHead head = this.getById(id);
+ OnlCgreportHead head = getOwnedHead(id);
if (ObjectUtil.isEmpty(head)) throw new RuntimeException("数据为空,查询失败");
String sql = head.getCgrSql();
//todo sql动态替换值
diff --git a/common/common-generator/src/main/java/apelet/common/generator/service/impl/OnlFormHeadServiceImpl.java b/common/common-generator/src/main/java/apelet/common/generator/service/impl/OnlFormHeadServiceImpl.java
index a4b789b..1e11e89 100644
--- a/common/common-generator/src/main/java/apelet/common/generator/service/impl/OnlFormHeadServiceImpl.java
+++ b/common/common-generator/src/main/java/apelet/common/generator/service/impl/OnlFormHeadServiceImpl.java
@@ -2,6 +2,7 @@ package apelet.common.generator.service.impl;
import apelet.common.core.cache.CacheKey;
import apelet.common.core.constant.GlobalDeletedFlag;
+import apelet.common.core.exception.DataNotFoundException;
import apelet.common.core.object.TokenData;
import apelet.common.dict.model.GlobalDictItem;
import apelet.common.dict.service.GlobalDictItemService;
@@ -51,6 +52,7 @@ import java.sql.ResultSet;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@@ -132,6 +134,55 @@ public class OnlFormHeadServiceImpl extends ServiceImplapp_code 为可选维度:请求未携带 appCode 时不校验(FR-4.14)。
+ *
+ * 无请求上下文(MQ 消费、定时任务)不做校验,否则租户同步链路无法删除/更新租户数据。
+ *
+ * @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 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
public OnlFormHeadVo listByHeadId(Long id) {
@@ -146,6 +197,15 @@ public class OnlFormHeadServiceImpl extends ServiceImpl list = this.list(new LambdaQueryWrapper()
- .eq(OnlFormHead::getTableName, name)
- );
+ LambdaQueryWrapper mainWrapper = new LambdaQueryWrapper()
+ .eq(OnlFormHead::getTableName, name);
+ applyTenantScope(mainWrapper);
+ List list = this.list(mainWrapper);
if (ObjectUtil.isNotEmpty(list)) {
for (OnlFormHead onlFormHead : list) {
String childName = onlFormHead.getChildName();
@@ -196,6 +257,18 @@ public class OnlFormHeadServiceImpl extends ServiceImpl().eq(OnlFormHead::getTableName, name)
- );
+ LambdaQueryWrapper mainWrapper = new LambdaQueryWrapper()
+ .eq(OnlFormHead::getTableName, name);
+ applyTenantScope(mainWrapper);
+ OnlFormHead onlFormHead = this.getOne(mainWrapper);
if (onlFormHead!=null) {
// BeanUtil.copyProperties(dto,onlFormHead);
String childName = onlFormHead.getChildName();
@@ -236,6 +310,9 @@ public class OnlFormHeadServiceImpl extends ServiceImpl list = this.list(new LambdaQueryWrapper().eq(OnlFormHead::getTableName, tableName));
+ //同一租户内判重,避免跨租户误判表名已存在(PRD FR-4.15)
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper()
+ .eq(OnlFormHead::getTableName, tableName);
+ applyTenantScope(wrapper);
+ List list = this.list(wrapper);
return list.isEmpty();
}
@@ -928,7 +1027,11 @@ public class OnlFormHeadServiceImpl extends ServiceImpl sub = this.list(new LambdaQueryWrapper()
- .in(OnlFormHead::getTableName, main.getChildName().split(","))
- );
+ LambdaQueryWrapper subWrapper = new LambdaQueryWrapper()
+ .in(OnlFormHead::getTableName, main.getChildName().split(","));
+ applyTenantScope(subWrapper);
+ List sub = this.list(subWrapper);
if (ObjectUtil.isNotEmpty(sub)) {
result.put("sub", sub);
List subList = new ArrayList<>();
@@ -999,7 +1103,123 @@ public class OnlFormHeadServiceImpl extends ServiceImpl规则与任务清单里的“已确认的关键决策”一致:
+ *
+ * - 提交里有、库里没有 → 插入;
+ * - 提交里有、库里也有 → 更新;
+ * - 库里有、提交里没有 → 保留(不删)。这是本方法存在的全部意义:租户端只提交自己关心的字段时,
+ * 管理端下发但本次未提交的字段不会被物理删除(P-9 / R-2)。
+ *
+ *
+ * 为什么还需要回填 id:旧实现靠前面的 {@code removeChildData} 全删来保证“重插不产生重复行”。
+ * 去掉删除以后,若前端不传子行 id(新增行本来就没有 id),同一行重复提交会插出两行同名字段。
+ * 因此提交行没带 id(或带的 id 不属于当前 headId)时,按业务唯一键回填库里已有行的 id 后再走更新。
+ *
+ * 子表语义说明:六张子表都只通过 {@code head_id} 从属父表、没有独立的删除入口(N-6 本期不做删除,
+ * A 端只能隐藏),所以统一按“不删”处理,不区分是哪张子表。
+ *
+ * @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().eq(OnlFormField::getHeadId, headId)),
+ OnlFormField::getId, OnlFormField::setId, OnlFormField::getFieldName));
+ dto.setOnlFormFindList(this.mergeChildIds(dto.getOnlFormFindList(),
+ onlFormFindService.list(new LambdaQueryWrapper().eq(OnlFormFind::getHeadId, headId)),
+ OnlFormFind::getId, OnlFormFind::setId, OnlFormFind::getFieldName));
+ dto.setOnlFormCheckList(this.mergeChildIds(dto.getOnlFormCheckList(),
+ onlFormCheckService.list(new LambdaQueryWrapper().eq(OnlFormCheck::getHeadId, headId)),
+ OnlFormCheck::getId, OnlFormCheck::setId, OnlFormCheck::getFieldName));
+ dto.setOnlFormIndexList(this.mergeChildIds(dto.getOnlFormIndexList(),
+ onlFormIndexService.list(new LambdaQueryWrapper().eq(OnlFormIndex::getHeadId, headId)),
+ OnlFormIndex::getId, OnlFormIndex::setId, OnlFormIndex::getIndexName));
+ dto.setOnlFormKeyList(this.mergeChildIds(dto.getOnlFormKeyList(),
+ onlFormKeyService.list(new LambdaQueryWrapper().eq(OnlFormKey::getHeadId, headId)),
+ OnlFormKey::getId, OnlFormKey::setId, OnlFormKey::getFieldName));
+ dto.setOnlFormPageList(this.mergeChildIds(dto.getOnlFormPageList(),
+ onlFormPageService.list(new LambdaQueryWrapper().eq(OnlFormPage::getHeadId, headId)),
+ OnlFormPage::getId, OnlFormPage::setId, OnlFormPage::getFieldName));
+ // addChildData 内部用 saveOrUpdateBatch:id 有值走更新、无值走插入,正好匹配上面的回填结果。
+ addChildData(dto);
+ }
+
+ /**
+ * 为提交的子行决定最终 id,并按 id / 业务唯一键去重(同一行重复提交只保留最后一条)。
+ *
+ * 三种情况:
+ *
+ * - 提交行 id 已存在于当前 headId 下 → 保持 id,更新该行;
+ * - 提交行 id 为空、或该 id 不属于当前 headId(可能是伪造/串种)→ 先按业务唯一键找库里同名的行,
+ * 找到就用它的 id(更新),找不到就置空 id(插入)。不直接用提交进来的 id 落库,
+ * 否则可以把别的父表下的子行“搬”到当前父表(对照 FR-4.16 的归属校验)。
+ * - 同一次提交里出现两条同一业务唯一键 → 后者覆盖前者。
+ *
+ *
+ * @param submitted 本次提交的子行集合,允许为 null/空(返回 null,调用方不会删任何数据)。
+ * @param existing 库里当前 headId 下已有的子行。
+ * @param idGetter 子行 id 读取器。
+ * @param idSetter 子行 id 写回器(就地回填)。
+ * @param keyGetter 业务唯一键读取器(字段名 / 索引名)。
+ * @return 回填 id 并去重后的提交集合。
+ */
+ private List mergeChildIds(List submitted, List existing,
+ Function idGetter, BiConsumer idSetter,
+ Function keyGetter) {
+ if (ObjectUtil.isEmpty(submitted)) {
+ return submitted;
+ }
+ Set existingIds = new HashSet<>();
+ Map 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 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 void removeChildData(T id,boolean isync) {
onlFormFindService.remove(new LambdaQueryWrapper()
.eq(OnlFormFind::getHeadId, id)
@@ -1023,7 +1243,12 @@ public class OnlFormHeadServiceImpl extends ServiceImpl().eq(OnlFormFind::getHeadId, id)));
vo.setOnlFormFieldList(onlFormFieldService.queryOnlFormField(new LambdaQueryWrapper().eq(OnlFormField::getHeadId, id)));