().eq("form_id", dbOnlineForm.getFormId()));
+ }
+ //按照同步过来的datasourceIdSet更新数据
+ OnlineFormDatasource formDatasourceFilter = new OnlineFormDatasource();
+ formDatasourceFilter.setFormId(onlineForm.getFormId());
+ onlineFormDatasourceMapper.delete(new QueryWrapper<>(formDatasourceFilter));
+ if (CollUtil.isNotEmpty(datasourceIdSet)) {
+ for (Long datasourceId : datasourceIdSet) {
+ OnlineFormDatasource onlineFormDatasource = new OnlineFormDatasource();
+ onlineFormDatasource.setId(idGenerator.nextLongId());
+ onlineFormDatasource.setFormId(onlineForm.getFormId());
+ onlineFormDatasource.setDatasourceId(datasourceId);
+ onlineFormDatasourceMapper.insert(onlineFormDatasource);
+ }
+ }
+
+ }
+ }
+
+ @Override
+ public String handleMessageType() {
+ return OnlineForm.class.getSimpleName();
+ }
+
+ /**
+ * 返回关联的RocketMQ消费者服务。
+ * 该服务的实现类,将只是处理来自该消费者的消息。
+ *
+ * @return 关联的RocketMQ消费者服务。
+ */
+ @Override
+ public Class extends BaseDataSyncRocketMqConsumer> handleConsumerClass() {
+ return TenantSyncConsumer.class;
+ }
+}
diff --git a/application-tenant/tenant-sync/src/main/java/apelet/tenantsync/online/utils/FormWidgetMergeUtils.java b/application-tenant/tenant-sync/src/main/java/apelet/tenantsync/online/utils/FormWidgetMergeUtils.java
new file mode 100644
index 0000000..aaf1f4c
--- /dev/null
+++ b/application-tenant/tenant-sync/src/main/java/apelet/tenantsync/online/utils/FormWidgetMergeUtils.java
@@ -0,0 +1,909 @@
+package apelet.tenantsync.online.utils;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * form_widget.json 深度合并工具
+ *
+ * 合并策略:
+ *
+ * - 对象(JSONObject):递归深度合并,source 覆盖 target 的同名键
+ * - 数组(JSONArray):按业务键去重合并,具体逻辑由数组合并策略决定
+ * - 标量值:source 直接覆盖 target
+ *
+ *
+ */
+public class FormWidgetMergeUtils {
+
+ /**
+ * 数组合并策略枚举
+ */
+ public enum ArrayMergeStrategy {
+ /** 按业务键去重合并:相同键的元素由 source 覆盖 target,新增元素追加 */
+ MERGE_BY_KEY,
+ /** 直接追加:source 数组元素全部追加到 target 数组末尾 */
+ APPEND
+ }
+
+ /**
+ * 数组路径 → 业务键 映射配置
+ *
+ * key 为 JSON 路径中数组字段的名称,value 为该数组中元素的业务主键字段名
+ * 例如 "operationList" → "type" 表示 operationList 数组按 type 字段去重
+ */
+ private static final Map ARRAY_BUSINESS_KEY_MAP = new LinkedHashMap<>();
+
+ /**
+ * 数组路径 → 合并策略 映射配置
+ *
+ * 未在此映射中配置的数组字段,默认使用 APPEND 策略
+ */
+ private static final Map ARRAY_STRATEGY_MAP = new LinkedHashMap<>();
+
+ static {
+ // ====== 数组业务键配置 ======
+ // operationList 按 type 去重(如两个JSON都有 type=0 的"新建"按钮,则后者覆盖前者)
+ ARRAY_BUSINESS_KEY_MAP.put("operationList", "type");
+ // childWidgetList 按 variableName 去重(如两个JSON都有 billno 字段组件)
+ ARRAY_BUSINESS_KEY_MAP.put("childWidgetList", "variableName");
+ // pluginList 按 pluginType 去重
+ ARRAY_BUSINESS_KEY_MAP.put("pluginList", "pluginType");
+ // tableColumnList 按 columnFieldName 去重
+ ARRAY_BUSINESS_KEY_MAP.put("tableColumnList", "id");
+ // widgetList 按 variableName 去重
+ ARRAY_BUSINESS_KEY_MAP.put("widgetList", "variableName");
+ // otherWidgetList 按 variableName 去重
+ ARRAY_BUSINESS_KEY_MAP.put("otherWidgetList", "variableName");
+
+// ARRAY_BUSINESS_KEY_MAP.put("formEventList", "variableName");
+// ARRAY_BUSINESS_KEY_MAP.put("eventList", "variableName");
+// ARRAY_BUSINESS_KEY_MAP.put("paramList", "variableName");
+// ARRAY_BUSINESS_KEY_MAP.put("printParamList", "variableName");
+// ARRAY_BUSINESS_KEY_MAP.put("customFieldList", "variableName");
+// ARRAY_BUSINESS_KEY_MAP.put("exportColumnList", "variableName");
+
+
+
+ // ====== 数组合并策略配置 ======
+ // 以上配置了业务键的数组默认使用 MERGE_BY_KEY 策略
+ ARRAY_STRATEGY_MAP.put("operationList", ArrayMergeStrategy.MERGE_BY_KEY);
+ ARRAY_STRATEGY_MAP.put("childWidgetList", ArrayMergeStrategy.MERGE_BY_KEY);
+ ARRAY_STRATEGY_MAP.put("pluginList", ArrayMergeStrategy.MERGE_BY_KEY);
+ ARRAY_STRATEGY_MAP.put("tableColumnList", ArrayMergeStrategy.MERGE_BY_KEY);
+ ARRAY_STRATEGY_MAP.put("widgetList", ArrayMergeStrategy.MERGE_BY_KEY);
+ ARRAY_STRATEGY_MAP.put("otherWidgetList", ArrayMergeStrategy.MERGE_BY_KEY);
+ }
+
+ private FormWidgetMergeUtils() {
+ }
+
+ /**
+ * 三路合并:在保留 base→branch1 修改的基础上,合并 base→branch2 的修改
+ *
+ * 合并逻辑:
+ * 1. 计算 branch1 相对于 base 的差异(diff1)
+ * 2. 计算 branch2 相对于 base 的差异(diff2)
+ * 3. 将 diff1 应用到 base 上
+ * 4. 将 diff2 应用到结果上(不覆盖 diff1 已修改的部分)
+ *
+ * @param base 基础版本JSON(如 1.0)
+ * @param branch1 第一个分支JSON(如 1.5)
+ * @param branch2 第二个分支JSON(如 2.0)
+ * @return 三路合并后的JSON对象
+ */
+ public static JSONObject threeWayMerge(JSONObject base, JSONObject branch1, JSONObject branch2) {
+ if (base == null) {
+ // 如果没有基础版本,直接合并 branch1 和 branch2, 以 branch1 为准
+ return merge(branch2, branch1);
+ }
+ if (branch1 == null && branch2 == null) {
+ return deepCopy(base);
+ }
+ if (branch1 == null) {
+ return deepCopy(branch2);
+ }
+ if (branch2 == null) {
+ return deepCopy(branch1);
+ }
+
+ // 步骤1:计算 branch1 相对于 base 的差异
+ JSONObject diff1 = computeDiff(base, branch1, "");
+
+ // 步骤2:计算 branch2 相对于 base 的差异
+ JSONObject diff2 = computeDiff(base, branch2, "");
+
+ // 步骤3:先将 branch1 的差异应用到 base
+ JSONObject result = deepCopy(base);
+ deepMergeObject(result, diff1, "");
+
+ // 步骤4:将 branch2 的差异应用到结果,但不覆盖 diff1 已修改的部分
+ applyDiffWithoutOverride(result, diff2, diff1, "");
+
+ return result;
+ }
+
+ /**
+ * 计算 source 相对于 base 的差异
+ *
+ * 返回一个包含所有新增和修改字段的JSONObject,删除的字段也会被标记
+ *
+ * @param base 基础版本
+ * @param source 目标版本
+ * @param path 当前路径
+ * @return 差异对象
+ */
+ private static JSONObject computeDiff(JSONObject base, JSONObject source, String path) {
+ JSONObject diff = new JSONObject();
+
+ // 检查 source 中新增或修改的字段
+ for (Map.Entry entry : source.entrySet()) {
+ String key = entry.getKey();
+ Object sourceValue = entry.getValue();
+ String currentPath = buildPath(path, key);
+
+ if (!base.containsKey(key)) {
+ // 新增字段
+ diff.put(key, deepCopyValue(sourceValue));
+ } else {
+ // 字段已存在,检查是否有变化
+ Object baseValue = base.get(key);
+ if (!valuesEqual(baseValue, sourceValue)) {
+ // 值发生变化
+ if (baseValue instanceof JSONObject && sourceValue instanceof JSONObject) {
+ // 对象类型,递归计算差异
+ JSONObject subDiff = computeDiff((JSONObject) baseValue, (JSONObject) sourceValue, currentPath);
+ if (!subDiff.isEmpty()) {
+ diff.put(key, subDiff);
+ }
+ } else if (baseValue instanceof JSONArray && sourceValue instanceof JSONArray) {
+ // 数组类型,计算数组差异
+ JSONArray arrayDiff = computeArrayDiff((JSONArray) baseValue, (JSONArray) sourceValue, currentPath);
+ if (arrayDiff != null && !arrayDiff.isEmpty()) {
+ diff.put(key, arrayDiff);
+ }
+ } else {
+ // 标量值变化,直接记录
+ diff.put(key, deepCopyValue(sourceValue));
+ }
+ }
+ }
+ }
+
+ // 检查 base 中有但 source 中删除的字段
+ for (String key : base.keySet()) {
+ if (!source.containsKey(key)) {
+ // 使用特殊标记表示删除(使用 null 值)
+ diff.put(key, null);
+ }
+ }
+
+ return diff;
+ }
+
+ /**
+ * 计算两个数组的差异
+ *
+ * @param baseArray 基础数组
+ * @param sourceArray 目标数组
+ * @param currentPath 当前路径
+ * @return 差异数组,如果无差异返回null
+ */
+ private static JSONArray computeArrayDiff(JSONArray baseArray, JSONArray sourceArray, String currentPath) {
+ String arrayFieldName = extractFieldName(currentPath);
+ ArrayMergeStrategy strategy = ARRAY_STRATEGY_MAP.getOrDefault(arrayFieldName, ArrayMergeStrategy.APPEND);
+
+ if (strategy == ArrayMergeStrategy.MERGE_BY_KEY) {
+ return computeArrayDiffByKey(baseArray, sourceArray, arrayFieldName);
+ } else {
+ // 对于 APPEND 和 REPLACE 策略,如果数组不同则返回整个 source 数组
+ if (!arraysEqual(baseArray, sourceArray)) {
+ return deepCopy(sourceArray);
+ }
+ return null;
+ }
+ }
+
+ /**
+ * 按业务键计算数组差异
+ */
+ private static JSONArray computeArrayDiffByKey(JSONArray baseArray, JSONArray sourceArray, String arrayFieldName) {
+ String businessKey = ARRAY_BUSINESS_KEY_MAP.get(arrayFieldName);
+ if (businessKey == null) {
+ if (!arraysEqual(baseArray, sourceArray)) {
+ return deepCopy(sourceArray);
+ }
+ return null;
+ }
+
+ JSONArray diff = new JSONArray();
+ Map baseKeyIndexMap = buildKeyIndexMap(baseArray, businessKey);
+ Map sourceKeyIndexMap = buildKeyIndexMap(sourceArray, businessKey);
+
+ // 检查 source 中新增或修改的元素
+ for (Map.Entry entry : sourceKeyIndexMap.entrySet()) {
+ String keyValue = entry.getKey();
+ int sourceIndex = entry.getValue();
+ Object sourceElement = sourceArray.get(sourceIndex);
+
+ Integer baseIndex = baseKeyIndexMap.get(keyValue);
+ if (baseIndex == null) {
+ // 新增元素
+ diff.add(deepCopyValue(sourceElement));
+ } else {
+ // 元素已存在,检查是否有变化
+ Object baseElement = baseArray.get(baseIndex);
+ if (!valuesEqual(baseElement, sourceElement)) {
+ if (baseElement instanceof JSONObject && sourceElement instanceof JSONObject) {
+ JSONObject subDiff = computeDiff((JSONObject) baseElement, (JSONObject) sourceElement,
+ arrayFieldName + "[" + keyValue + "]");
+ if (!subDiff.isEmpty()) {
+ // 将差异包装成包含业务键的对象
+ JSONObject wrappedDiff = new JSONObject();
+ wrappedDiff.put(businessKey, keyValue);
+ for (Map.Entry diffEntry : subDiff.entrySet()) {
+ if (!diffEntry.getKey().equals(businessKey)) {
+ wrappedDiff.put(diffEntry.getKey(), diffEntry.getValue());
+ }
+ }
+ diff.add(wrappedDiff);
+ }
+ } else {
+ // 非对象元素变化
+ diff.add(deepCopyValue(sourceElement));
+ }
+ }
+ }
+ }
+
+ // 检查 base 中删除的元素
+ for (Map.Entry entry : baseKeyIndexMap.entrySet()) {
+ String keyValue = entry.getKey();
+ if (!sourceKeyIndexMap.containsKey(keyValue)) {
+ // 删除的元素,用只包含业务键的对象标记
+ JSONObject deleteMarker = new JSONObject();
+ deleteMarker.put(businessKey, keyValue);
+ deleteMarker.put("__deleted__", true);
+ diff.add(deleteMarker);
+ }
+ }
+
+ return diff.isEmpty() ? null : diff;
+ }
+
+ /**
+ * 应用差异但不覆盖已修改的部分
+ *
+ * 这是三路合并的核心逻辑:确保 branch2 的差异不会覆盖 branch1 已修改的内容
+ *
+ * @param target 目标对象(会被修改)
+ * @param diff 要应用的差异
+ * @param diff1 branch1 的差异(用于检测冲突)
+ * @param path 当前路径
+ */
+ private static void applyDiffWithoutOverride(JSONObject target, JSONObject diff, JSONObject diff1, String path) {
+ for (Map.Entry entry : diff.entrySet()) {
+ String key = entry.getKey();
+ Object diffValue = entry.getValue();
+ String currentPath = buildPath(path, key);
+
+ // 检查这个字段是否在 diff1 中也被修改了(冲突检测)
+ boolean conflictInDiff1 = diff1 != null && diff1.containsKey(key);
+
+ if (diffValue == null) {
+ // 删除操作:仅在 diff1 中没有修改此字段时才执行删除
+ if (!conflictInDiff1) {
+ target.remove(key);
+ }
+ // 如果 diff1 也修改了此字段,保留 diff1 的修改
+ } else if (diffValue instanceof JSONObject) {
+ // 对象差异
+ if (!target.containsKey(key)) {
+ target.put(key, deepCopyValue(diffValue));
+ } else {
+ Object targetValue = target.get(key);
+ if (targetValue instanceof JSONObject) {
+ JSONObject targetObj = (JSONObject) targetValue;
+ JSONObject diff1Sub = conflictInDiff1 && diff1.get(key) instanceof JSONObject
+ ? (JSONObject) diff1.get(key) : null;
+ applyDiffWithoutOverride(targetObj, (JSONObject) diffValue, diff1Sub, currentPath);
+ } else {
+ // 类型不匹配,直接覆盖
+ target.put(key, deepCopyValue(diffValue));
+ }
+ }
+ } else if (diffValue instanceof JSONArray) {
+ // 数组差异
+ if (!target.containsKey(key)) {
+ target.put(key, deepCopyValue(diffValue));
+ } else {
+ Object targetValue = target.get(key);
+ if (targetValue instanceof JSONArray) {
+ // 合并数组差异
+ JSONArray mergedArray = mergeArrayDiff((JSONArray) targetValue, (JSONArray) diffValue,
+ conflictInDiff1 && diff1.get(key) instanceof JSONArray
+ ? (JSONArray) diff1.get(key) : null,
+ currentPath);
+ target.put(key, mergedArray);
+ } else {
+ // 类型不匹配,直接覆盖
+ target.put(key, deepCopyValue(diffValue));
+ }
+ }
+ } else {
+ // 标量值差异:仅在 diff1 中没有修改此字段时才应用
+ if (!conflictInDiff1) {
+ target.put(key, deepCopyValue(diffValue));
+ }
+ // 如果 diff1 也修改了此字段,保留 diff1 的修改(不覆盖)
+ }
+ }
+ }
+
+ /**
+ * 合并数组差异
+ */
+ private static JSONArray mergeArrayDiff(JSONArray targetArray, JSONArray diff, JSONArray diff1, String currentPath) {
+ String arrayFieldName = extractFieldName(currentPath);
+ ArrayMergeStrategy strategy = ARRAY_STRATEGY_MAP.getOrDefault(arrayFieldName, ArrayMergeStrategy.APPEND);
+
+ if (strategy == ArrayMergeStrategy.MERGE_BY_KEY) {
+ return mergeArrayDiffByKey(targetArray, diff, diff1, arrayFieldName);
+ } else {
+ // 对于简单策略,直接应用 diff
+ JSONArray result = deepCopy(targetArray);
+ result.addAll(deepCopy(diff));
+ return result;
+ }
+ }
+
+ /**
+ * 按业务键合并数组差异
+ */
+ private static JSONArray mergeArrayDiffByKey(JSONArray targetArray, JSONArray diff, JSONArray diff1,
+ String arrayFieldName) {
+ String businessKey = ARRAY_BUSINESS_KEY_MAP.get(arrayFieldName);
+ if (businessKey == null) {
+ JSONArray result = deepCopy(targetArray);
+ result.addAll(deepCopy(diff));
+ return result;
+ }
+
+ JSONArray result = deepCopy(targetArray);
+ Map targetKeyIndexMap = buildKeyIndexMap(result, businessKey);
+ // 构建 diff1 的业务键到元素的映射(而不是索引映射)
+ Map diff1ElementMap = diff1 != null ? buildKeyElementMap(diff1, businessKey) : null;
+
+ for (int i = 0; i < diff.size(); i++) {
+ Object diffElement = diff.get(i);
+
+ if (!(diffElement instanceof JSONObject)) {
+ result.add(deepCopyValue(diffElement));
+ continue;
+ }
+
+ JSONObject diffObj = (JSONObject) diffElement;
+
+ // 检查是否是删除标记
+ if (Boolean.TRUE.equals(diffObj.getBoolean("__deleted__"))) {
+ String keyValue = extractBusinessKeyValue(diffObj, businessKey);
+ if (keyValue != null) {
+ // 仅在 diff1 中没有修改此元素时才执行删除
+ if (diff1ElementMap == null || !diff1ElementMap.containsKey(keyValue)) {
+ Integer targetIndex = targetKeyIndexMap.get(keyValue);
+ if (targetIndex != null) {
+ result.remove(targetIndex.intValue());
+ // 重建索引映射
+ targetKeyIndexMap = buildKeyIndexMap(result, businessKey);
+ }
+ }
+ }
+ continue;
+ }
+
+ String keyValue = extractBusinessKeyValue(diffObj, businessKey);
+ if (keyValue == null) {
+ result.add(deepCopyValue(diffElement));
+ continue;
+ }
+
+ Integer targetIndex = targetKeyIndexMap.get(keyValue);
+ if (targetIndex != null) {
+ // 元素已存在,递归合并
+ Object targetElement = result.get(targetIndex);
+ if (targetElement instanceof JSONObject) {
+ JSONObject targetObj = (JSONObject) targetElement;
+ // 检查 diff1 中是否也修改了此元素(获取完整的元素对象)
+ JSONObject diff1Element = diff1ElementMap != null ? diff1ElementMap.get(keyValue) : null;
+ applyDiffWithoutOverride(targetObj, diffObj, diff1Element, arrayFieldName + "[" + keyValue + "]");
+ }
+ } else {
+ // 新增元素
+ result.add(deepCopyValue(diffElement));
+ targetKeyIndexMap.put(keyValue, result.size() - 1);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * 构建业务键到元素对象的映射(用于三路合并冲突检测)
+ */
+ private static Map buildKeyElementMap(JSONArray array, String businessKey) {
+ Map map = new LinkedHashMap<>();
+ for (int i = 0; i < array.size(); i++) {
+ Object element = array.get(i);
+ if (!(element instanceof JSONObject)) {
+ continue;
+ }
+ JSONObject obj = (JSONObject) element;
+ String keyValue = extractBusinessKeyValue(obj, businessKey);
+ if (keyValue != null) {
+ map.put(keyValue, obj);
+ }
+ }
+ return map;
+ }
+
+ /**
+ * 比较两个值是否相等
+ */
+ private static boolean valuesEqual(Object value1, Object value2) {
+ if (value1 == null && value2 == null) {
+ return true;
+ }
+ if (value1 == null || value2 == null) {
+ return false;
+ }
+ if (value1 instanceof JSONObject && value2 instanceof JSONObject) {
+ return ((JSONObject) value1).toJSONString().equals(((JSONObject) value2).toJSONString());
+ }
+ if (value1 instanceof JSONArray && value2 instanceof JSONArray) {
+ return ((JSONArray) value1).toJSONString().equals(((JSONArray) value2).toJSONString());
+ }
+ return value1.equals(value2);
+ }
+
+ /**
+ * 比较两个数组是否相等
+ */
+ private static boolean arraysEqual(JSONArray arr1, JSONArray arr2) {
+ if (arr1 == null && arr2 == null) {
+ return true;
+ }
+ if (arr1 == null || arr2 == null) {
+ return false;
+ }
+ return arr1.toJSONString().equals(arr2.toJSONString());
+ }
+
+ // ==================== 公开入口方法 ====================
+
+ /**
+ * 合并两个 form_widget JSON 对象(source 覆盖 target)
+ *
+ * @param target 基础JSON(被覆盖方)
+ * @param source 覆盖JSON(覆盖方)
+ * @return 合并后的JSON对象(新对象,不修改入参)
+ */
+ public static JSONObject merge(JSONObject target, JSONObject source) {
+ if (target == null) {
+ return source != null ? deepCopy(source) : new JSONObject();
+ }
+ if (source == null) {
+ return deepCopy(target);
+ }
+ JSONObject result = deepCopy(target);
+ deepMergeObject(result, source, "");
+ return result;
+ }
+
+ /**
+ * 从 InputStream 读取并合并两个 JSON
+ *
+ * @param baseStream 基础JSON输入流
+ * @param overrideStream 覆盖JSON输入流
+ * @return 合并后的JSON对象
+ */
+ public static JSONObject merge(InputStream baseStream, InputStream overrideStream) {
+ JSONObject target = readJsonFromStream(baseStream);
+ JSONObject source = readJsonFromStream(overrideStream);
+ return merge(target, source);
+ }
+
+ /**
+ * 合并两个 JSON 字符串
+ *
+ * @param baseJson 基础JSON字符串
+ * @param overrideJson 覆盖JSON字符串
+ * @return 合并后的JSON对象
+ */
+ public static JSONObject merge(String baseJson, String overrideJson) {
+ JSONObject target = JSON.parseObject(baseJson);
+ JSONObject source = JSON.parseObject(overrideJson);
+ return merge(target, source);
+ }
+
+ /**
+ * 仅合并指定端(pc 或 mobile)的配置
+ *
+ * @param target 基础JSON
+ * @param source 覆盖JSON
+ * @param endpoint 端标识:"pc" 或 "mobile"
+ * @return 合并后的JSON对象
+ */
+ public static JSONObject mergeEndpoint(JSONObject target, JSONObject source, String endpoint) {
+ if (endpoint == null || (!"pc".equals(endpoint) && !"mobile".equals(endpoint))) {
+ throw new IllegalArgumentException("endpoint 必须为 'pc' 或 'mobile'");
+ }
+ JSONObject result = deepCopy(target);
+ if (!source.containsKey(endpoint)) {
+ return result;
+ }
+ JSONObject sourceEndpoint = source.getJSONObject(endpoint);
+ if (sourceEndpoint == null) {
+ return result;
+ }
+ JSONObject targetEndpoint = result.getJSONObject(endpoint);
+ if (targetEndpoint == null) {
+ result.put(endpoint, deepCopy(sourceEndpoint));
+ } else {
+ deepMergeObject(targetEndpoint, sourceEndpoint, endpoint);
+ }
+ return result;
+ }
+
+ // ==================== 核心递归合并逻辑 ====================
+
+ /**
+ * 递归深度合并:将 source 的内容合并到 target 中(target 会被直接修改)
+ *
+ * @param target 目标对象(会被修改)
+ * @param source 源对象(覆盖方)
+ * @param path 当前JSON路径,用于定位上下文和策略选择
+ */
+ private static void deepMergeObject(JSONObject target, JSONObject source, String path) {
+ for (Map.Entry entry : source.entrySet()) {
+ String key = entry.getKey();
+ Object sourceValue = entry.getValue();
+ String currentPath = buildPath(path, key);
+
+ if (!target.containsKey(key)) {
+ // target 中不存在该键,直接放入(深拷贝)
+ target.put(key, deepCopyValue(sourceValue));
+ } else {
+ // target 中已存在该键,按类型决定合并方式
+ Object targetValue = target.get(key);
+ target.put(key, mergeValues(targetValue, sourceValue, currentPath));
+ }
+ }
+ }
+
+ /**
+ * 合并两个值(根据类型分发到不同的合并策略)
+ *
+ * @param targetValue 目标值
+ * @param sourceValue 源值
+ * @param currentPath 当前路径
+ * @return 合并后的值
+ */
+ private static Object mergeValues(Object targetValue, Object sourceValue, String currentPath) {
+ // 两者都是 JSONObject → 递归合并
+ if (targetValue instanceof JSONObject && sourceValue instanceof JSONObject) {
+ JSONObject mergedObj = deepCopy((JSONObject) targetValue);
+ deepMergeObject(mergedObj, (JSONObject) sourceValue, currentPath);
+ return mergedObj;
+ }
+
+ // 两者都是 JSONArray → 按策略合并
+ if (targetValue instanceof JSONArray && sourceValue instanceof JSONArray) {
+ return mergeArray((JSONArray) targetValue, (JSONArray) sourceValue, currentPath);
+ }
+
+ // 类型不一致 或 标量值 → source 覆盖 target
+ return deepCopyValue(sourceValue);
+ }
+
+ // ==================== 数组合并逻辑 ====================
+
+ /**
+ * 合并两个 JSONArray(根据当前路径对应的策略决定合并方式)
+ *
+ * @param targetArray 目标数组
+ * @param sourceArray 源数组
+ * @param currentPath 当前JSON路径(用于匹配策略)
+ * @return 合并后的数组
+ */
+ private static JSONArray mergeArray(JSONArray targetArray, JSONArray sourceArray, String currentPath) {
+ // 从路径中提取数组字段名(取最后一段)
+ String arrayFieldName = extractFieldName(currentPath);
+ ArrayMergeStrategy strategy = ARRAY_STRATEGY_MAP.getOrDefault(arrayFieldName, ArrayMergeStrategy.APPEND);
+
+ switch (strategy) {
+ case MERGE_BY_KEY:
+ return mergeArrayByKey(targetArray, sourceArray, arrayFieldName);
+ case APPEND:
+ return mergeArrayAppend(targetArray, sourceArray);
+ default:
+ return mergeArrayAppend(targetArray, sourceArray);
+ }
+ }
+
+ /**
+ * 按业务键去重合并数组
+ *
+ * 逻辑:
+ * 1. 从 ARRAY_BUSINESS_KEY_MAP 中获取该数组字段对应的业务主键名
+ * 2. 遍历 targetArray,建立 业务键 → 索引 的映射
+ * 3. 遍历 sourceArray:
+ * - 若业务键已存在于 target 中,递归合并对应元素
+ * - 若业务键不存在,追加到结果数组末尾
+ *
+ * @param targetArray 目标数组
+ * @param sourceArray 源数组
+ * @param arrayFieldName 数组字段名(用于查找业务键配置)
+ * @return 合并后的数组
+ */
+ private static JSONArray mergeArrayByKey(JSONArray targetArray, JSONArray sourceArray, String arrayFieldName) {
+ String businessKey = ARRAY_BUSINESS_KEY_MAP.get(arrayFieldName);
+ if (businessKey == null) {
+ // 未配置业务键,降级为追加
+ return mergeArrayAppend(targetArray, sourceArray);
+ }
+
+ // 步骤1:深拷贝 targetArray 作为结果容器
+ JSONArray result = deepCopy(targetArray);
+
+ // 步骤2:构建 targetArray 的 业务键值 → 索引 映射
+ // - 仅处理 JSONObject 类型的元素(跳过标量元素)
+ // - 使用 String 业务键值作为 map key,支持 Number/Boolean 等类型自动转为字符串比较
+ Map targetKeyIndexMap = buildKeyIndexMap(result, businessKey);
+
+ // 步骤3:遍历 sourceArray,按业务键匹配合并或追加
+ for (int i = 0; i < sourceArray.size(); i++) {
+ Object sourceElement = sourceArray.get(i);
+
+ // 非 JSONObject 元素:直接追加(无法按业务键匹配)
+ if (!(sourceElement instanceof JSONObject)) {
+ result.add(deepCopyValue(sourceElement));
+ continue;
+ }
+
+ JSONObject sourceObj = (JSONObject) sourceElement;
+ String keyValue = extractBusinessKeyValue(sourceObj, businessKey);
+
+ if (keyValue == null) {
+ // 源元素不含业务键字段,无法匹配,直接追加
+ result.add(deepCopyValue(sourceElement));
+ continue;
+ }
+
+ Integer targetIndex = targetKeyIndexMap.get(keyValue);
+ if (targetIndex != null) {
+ // 业务键匹配到:递归合并 target 中对应索引的元素
+ Object targetElement = result.get(targetIndex);
+ if (targetElement instanceof JSONObject) {
+ // 对象级递归合并,保留 target 中的独有字段,source 覆盖同名字段
+ deepMergeObject((JSONObject) targetElement, sourceObj,
+ arrayFieldName + "[" + keyValue + "]");
+ } else {
+ // target 中对应元素不是对象(理论上不会发生),直接替换
+ result.set(targetIndex, deepCopyValue(sourceElement));
+ }
+ } else {
+ // 业务键未匹配:追加新元素
+ result.add(deepCopyValue(sourceElement));
+ // 同步更新索引映射,防止 source 内部出现重复业务键时后者覆盖前者
+ targetKeyIndexMap.put(keyValue, result.size() - 1);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * 构建 JSONArray 中各 JSONObject 元素的 业务键值 → 索引 映射
+ *
+ * 仅处理 JSONObject 类型的元素;标量元素跳过。
+ * 若同一业务键值出现多次,后出现的索引覆盖先前的(保留最后一个)。
+ *
+ * @param array JSON数组
+ * @param businessKey 业务主键字段名
+ * @return 业务键值字符串 → 在数组中的索引位置
+ */
+ private static Map buildKeyIndexMap(JSONArray array, String businessKey) {
+ Map map = new LinkedHashMap<>();
+ for (int i = 0; i < array.size(); i++) {
+ Object element = array.get(i);
+ if (!(element instanceof JSONObject)) {
+ continue;
+ }
+ String keyValue = extractBusinessKeyValue((JSONObject) element, businessKey);
+ if (keyValue != null) {
+ map.put(keyValue, i);
+ }
+ }
+ return map;
+ }
+
+ /**
+ * 提取 JSONObject 中指定业务键字段的值,统一转为字符串用于比较
+ *
+ * 支持的值类型:
+ *
+ * - String → 直接返回
+ * - Number → 返回其字符串表示(如 1 → "1", 10 → "10")
+ * - Boolean → 返回 "true" / "false"
+ * - null 或字段不存在 → 返回 null
+ *
+ *
+ * @param obj JSON对象
+ * @param businessKey 业务主键字段名
+ * @return 业务键值的字符串表示,字段不存在或值为null时返回null
+ */
+ private static String extractBusinessKeyValue(JSONObject obj, String businessKey) {
+ Object value = obj.get(businessKey);
+ if (value == null) {
+ return null;
+ }
+ // Number 类型需特殊处理:确保数值比较一致
+ // 例如 type=1 和 type="1" 应视为相同业务键
+ if (value instanceof Number) {
+ return String.valueOf(((Number) value).longValue());
+ }
+ return value.toString();
+ }
+
+ /**
+ * 追加合并:source 数组元素全部追加到 target 数组末尾
+ */
+ private static JSONArray mergeArrayAppend(JSONArray targetArray, JSONArray sourceArray) {
+ JSONArray result = deepCopy(targetArray);
+ result.addAll(deepCopy(sourceArray));
+ return result;
+ }
+
+ // ==================== 工具方法 ====================
+
+ /**
+ * 构建JSON路径(类似 $.pc.tableWidget.operationList)
+ */
+ private static String buildPath(String parentPath, String key) {
+ if (parentPath == null || parentPath.isEmpty()) {
+ return key;
+ }
+ return parentPath + "." + key;
+ }
+
+ /**
+ * 从路径中提取数组字段名
+ * 例如 "pc.tableWidget.operationList" → "operationList"
+ */
+ private static String extractFieldName(String path) {
+ if (path == null || path.isEmpty()) {
+ return "";
+ }
+ int lastDot = path.lastIndexOf('.');
+ return lastDot >= 0 ? path.substring(lastDot + 1) : path;
+ }
+
+ /**
+ * 深拷贝 JSONObject
+ */
+ private static JSONObject deepCopy(JSONObject obj) {
+ if (obj == null) {
+ return null;
+ }
+ return JSON.parseObject(obj.toJSONString());
+ }
+
+ /**
+ * 深拷贝 JSONArray
+ */
+ private static JSONArray deepCopy(JSONArray arr) {
+ if (arr == null) {
+ return null;
+ }
+ return JSON.parseArray(arr.toJSONString());
+ }
+
+ /**
+ * 深拷贝任意 JSON 值
+ */
+ @SuppressWarnings("unchecked")
+ private static Object deepCopyValue(Object value) {
+ if (value == null) {
+ return null;
+ }
+ if (value instanceof JSONObject) {
+ return deepCopy((JSONObject) value);
+ }
+ if (value instanceof JSONArray) {
+ return deepCopy((JSONArray) value);
+ }
+ // 标量值(String, Number, Boolean)不可变,直接返回
+ return value;
+ }
+
+ /**
+ * 从输入流读取 JSON
+ */
+ private static JSONObject readJsonFromStream(InputStream stream) {
+ try {
+ String content = new String(readAllBytes(stream), StandardCharsets.UTF_8);
+ return JSON.parseObject(content);
+ } catch (Exception e) {
+ throw new RuntimeException("读取JSON流失败", e);
+ }
+ }
+
+ /**
+ * 读取输入流全部字节(兼容 Java 8,不使用 InputStream.readAllBytes)
+ */
+ private static byte[] readAllBytes(InputStream stream) throws Exception {
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ byte[] data = new byte[4096];
+ int nRead;
+ while ((nRead = stream.read(data, 0, data.length)) != -1) {
+ buffer.write(data, 0, nRead);
+ }
+ return buffer.toByteArray();
+ }
+
+ /**
+ * 简易 ByteArrayOutputStream(避免额外依赖)
+ */
+ private static class ByteArrayOutputStream {
+ private final List buffers = new ArrayList<>();
+ private int count = 0;
+ private static final int BLOCK_SIZE = 4096;
+
+ void write(byte[] b, int off, int len) {
+ byte[] block = new byte[len];
+ System.arraycopy(b, off, block, 0, len);
+ buffers.add(block);
+ count += len;
+ }
+
+ byte[] toByteArray() {
+ byte[] result = new byte[count];
+ int pos = 0;
+ for (byte[] block : buffers) {
+ System.arraycopy(block, 0, result, pos, block.length);
+ pos += block.length;
+ }
+ return result;
+ }
+ }
+
+ // ==================== 策略配置方法(供外部动态调整) ====================
+
+ /**
+ * 注册数组的业务键配置
+ *
+ * @param arrayFieldName 数组字段名
+ * @param businessKey 业务主键字段名
+ */
+ public static void registerArrayBusinessKey(String arrayFieldName, String businessKey) {
+ ARRAY_BUSINESS_KEY_MAP.put(arrayFieldName, businessKey);
+ }
+
+ /**
+ * 注册数组的合并策略
+ *
+ * @param arrayFieldName 数组字段名
+ * @param strategy 合并策略
+ */
+ public static void registerArrayStrategy(String arrayFieldName, ArrayMergeStrategy strategy) {
+ ARRAY_STRATEGY_MAP.put(arrayFieldName, strategy);
+ }
+}
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 bb3eedd..e3b929f 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
@@ -50,4 +50,7 @@ public class OnlCgreportHead extends BaseModel implements Serializable {
@Schema(description = "逻辑删除标记(1: 正常 -1: 已删除)")
// @TableLogic
private Integer deletedFlag;
+
+ @Schema(description = "租户id")
+ private String tenantId;
}
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 8c3f2c9..258b5d1 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
@@ -114,6 +114,9 @@ public class OnlFormHead extends BaseModel implements Serializable {
@Schema(description = "继承表")
private String metaTable;
+ @Schema(description = "租户id")
+ private String tenantId;
+
@Schema(description = "数据库属性分录")
@TableField(exist = false)
private List onlFormFieldList;
diff --git a/common/common-online/src/main/java/apelet/common/online/model/OnlineForm.java b/common/common-online/src/main/java/apelet/common/online/model/OnlineForm.java
index 54dd673..51f1596 100644
--- a/common/common-online/src/main/java/apelet/common/online/model/OnlineForm.java
+++ b/common/common-online/src/main/java/apelet/common/online/model/OnlineForm.java
@@ -26,8 +26,9 @@ import java.util.Set;
*/
@Data
@TableName(value = "zz_online_form")
-public class OnlineForm {
+public class OnlineForm implements java.io.Serializable {
+ private static final long serialVersionUID = 1L;
/**
* 主键Id。
*/
diff --git a/common/common-online/src/main/java/apelet/common/online/service/impl/OnlineFormServiceImpl.java b/common/common-online/src/main/java/apelet/common/online/service/impl/OnlineFormServiceImpl.java
index 702bcf1..657736b 100644
--- a/common/common-online/src/main/java/apelet/common/online/service/impl/OnlineFormServiceImpl.java
+++ b/common/common-online/src/main/java/apelet/common/online/service/impl/OnlineFormServiceImpl.java
@@ -129,6 +129,8 @@ public class OnlineFormServiceImpl extends BaseService impleme
onlineForm.setCreateUserId(tokenData.getUserId());
onlineForm.setUpdateUserId(tokenData.getUserId());
onlineFormMapper.insert(onlineForm);
+ // 缓存上次更新的在线表单数据,新增则更新新增的表单数据
+ cacheLastUpdateOnlineForm(onlineForm);
if (CollUtil.isNotEmpty(datasourceIdSet)) {
for (Long datasourceId : datasourceIdSet) {
OnlineFormDatasource onlineFormDatasource = new OnlineFormDatasource();
@@ -178,6 +180,8 @@ public class OnlineFormServiceImpl extends BaseService impleme
onlineFormDatasourceMapper.insert(onlineFormDatasource);
}
}
+ // 缓存上次更新的在线表单数据,新增则更新新增的表单数据
+ cacheLastUpdateOnlineForm(originalOnlineForm);
return true;
}
@@ -198,6 +202,7 @@ public class OnlineFormServiceImpl extends BaseService impleme
OnlineFormDatasource formDatasourceFilter = new OnlineFormDatasource();
formDatasourceFilter.setFormId(formId);
onlineFormDatasourceMapper.delete(new QueryWrapper<>(formDatasourceFilter));
+ redissonClient.getBucket("lastUpdateOnlineForm:" + formId).delete();
return true;
}
@@ -220,6 +225,7 @@ public class OnlineFormServiceImpl extends BaseService impleme
for (Long formId : formIdSet) {
commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineFormKey(formId));
commonRedisUtil.evictFormCache(OnlineRedisKeyUtil.makeOnlineFormDatasourceKey(formId));
+ redissonClient.getBucket("lastUpdateOnlineForm:" + formId).delete();
}
}
return onlineFormMapper.delete(new QueryWrapper<>(filter));
@@ -624,4 +630,9 @@ public class OnlineFormServiceImpl extends BaseService impleme
}
}
+ public OnlineForm cacheLastUpdateOnlineForm(OnlineForm onlineForm) {
+ RBucket