20 changed files with 11816 additions and 6034 deletions
@ -0,0 +1,58 @@ |
|||||||
|
package apelet.tenantadmin.tenant.controller; |
||||||
|
|
||||||
|
import apelet.common.core.annotation.MyRequestBody; |
||||||
|
import apelet.common.core.constant.ErrorCodeEnum; |
||||||
|
import apelet.common.core.object.ResponseResult; |
||||||
|
import apelet.common.core.object.TokenData; |
||||||
|
import apelet.common.log.annotation.OperationLog; |
||||||
|
import apelet.common.log.model.constant.SysOperationLogType; |
||||||
|
import apelet.common.online.model.OnlineDatasource; |
||||||
|
import apelet.common.online.model.OnlineForm; |
||||||
|
import apelet.common.online.service.OnlineDatasourceService; |
||||||
|
import apelet.common.online.service.OnlineFormService; |
||||||
|
import apelet.tenantadmin.tenant.service.OnlineFormTenantSyncService; |
||||||
|
import cn.hutool.core.util.StrUtil; |
||||||
|
import lombok.extern.slf4j.Slf4j; |
||||||
|
import org.redisson.api.RBucket; |
||||||
|
import org.redisson.api.RedissonClient; |
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||||
|
import org.springframework.web.bind.annotation.RestController; |
||||||
|
|
||||||
|
import java.util.Collections; |
||||||
|
import java.util.HashSet; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Set; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
|
||||||
|
/** |
||||||
|
* 在线表单租户同步接口控制器类。 |
||||||
|
* |
||||||
|
* @author guifc |
||||||
|
* @date 2023-08-04 |
||||||
|
*/ |
||||||
|
@Slf4j |
||||||
|
@RestController |
||||||
|
@RequestMapping("/tenantadmin/tenant/onlineFormTenantSync") |
||||||
|
public class OnlineFormTenantSyncController { |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private OnlineFormTenantSyncService onlineFormTenantSyncService; |
||||||
|
|
||||||
|
/** |
||||||
|
* 同步更新租户的在线表单数据。 |
||||||
|
* |
||||||
|
* @param pageId 在线表单页面传输对象id |
||||||
|
* @param tenantIdList 租户ID列表 |
||||||
|
* @return 应答结果对象,包含同步操作是否成功的布尔值 |
||||||
|
*/ |
||||||
|
@OperationLog(type = SysOperationLogType.UPDATE) |
||||||
|
@PostMapping("/syncUpdateTenant") |
||||||
|
public ResponseResult<Boolean> syncUpdateTenant( |
||||||
|
@MyRequestBody Long pageId, |
||||||
|
@MyRequestBody List<Long> tenantIdList) { |
||||||
|
return ResponseResult.success(onlineFormTenantSyncService.syncUpdateTenant(pageId, tenantIdList)); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
package apelet.tenantadmin.tenant.service; |
||||||
|
|
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
public interface OnlineFormTenantSyncService { |
||||||
|
|
||||||
|
Boolean syncUpdateTenant(Long pageId, List<Long> tenantIdSet); |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,181 @@ |
|||||||
|
package apelet.tenantadmin.tenant.service.impl; |
||||||
|
|
||||||
|
import apelet.common.core.annotation.MyDataSource; |
||||||
|
import apelet.common.datasync.producer.DataSyncRocketMqProducer; |
||||||
|
import apelet.common.generator.model.*; |
||||||
|
import apelet.common.generator.service.*; |
||||||
|
import apelet.common.online.model.OnlineDatasource; |
||||||
|
import apelet.common.online.model.OnlineForm; |
||||||
|
import apelet.common.online.service.OnlineDatasourceService; |
||||||
|
import apelet.common.online.service.OnlineFormService; |
||||||
|
import apelet.common.tenant.constant.TenantConstant; |
||||||
|
import apelet.tenantadmin.config.ApplicationConfig; |
||||||
|
import apelet.tenantadmin.config.DataSourceType; |
||||||
|
import apelet.tenantadmin.tenant.service.OnlineFormTenantSyncService; |
||||||
|
import com.alibaba.fastjson.JSONObject; |
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
||||||
|
import com.jayway.jsonpath.JsonPath; |
||||||
|
import lombok.extern.slf4j.Slf4j; |
||||||
|
import org.jetbrains.annotations.NotNull; |
||||||
|
import org.redisson.api.RBucket; |
||||||
|
import org.redisson.api.RedissonClient; |
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.jdbc.core.BeanPropertyRowMapper; |
||||||
|
import org.springframework.jdbc.core.JdbcTemplate; |
||||||
|
import org.springframework.stereotype.Service; |
||||||
|
import org.springframework.transaction.annotation.Transactional; |
||||||
|
|
||||||
|
import java.util.Collections; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Set; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
|
||||||
|
/** |
||||||
|
* 租户权限资源数据服务实现类。 |
||||||
|
* |
||||||
|
* @author guifc |
||||||
|
* @date 2023-08-04 |
||||||
|
*/ |
||||||
|
@Slf4j |
||||||
|
@MyDataSource(DataSourceType.TENANT_ADMIN) |
||||||
|
@Service("onlineFormTenantSyncService") |
||||||
|
public class OnlineFormTenantSyncServiceImpl implements OnlineFormTenantSyncService { |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private OnlineFormService onlineFormService; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private RedissonClient redissonClient; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private OnlineDatasourceService onlineDatasourceService; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private IOnlCgreportHeadService iOnlCgreportHeadService; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private IOnlCgreportItemService iOnlCgreportItemService; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private IOnlCgreportParamService iOnlCgreportParamService; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private DataSyncRocketMqProducer dataSyncProducer; |
||||||
|
@Autowired |
||||||
|
private ApplicationConfig applicationConfig; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private JdbcTemplate jdbcTemplate; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private IOnlFormFindService onlFormFindService; |
||||||
|
@Autowired |
||||||
|
private IOnlFormFieldService onlFormFieldService; |
||||||
|
@Autowired |
||||||
|
private IOnlFormCheckService onlFormCheckService; |
||||||
|
@Autowired |
||||||
|
private IOnlFormIndexService onlFormIndexService; |
||||||
|
@Autowired |
||||||
|
private IOnlFormKeyService onlFormKeyService; |
||||||
|
@Autowired |
||||||
|
private IOnlFormPageService onlFormPageService; |
||||||
|
|
||||||
|
@Override |
||||||
|
@Transactional(rollbackFor = Exception.class) |
||||||
|
public Boolean syncUpdateTenant(Long pageId, List<Long> tenantIdSet) { |
||||||
|
List<OnlineForm> onlineFormListByPageIds = onlineFormService.getOnlineFormListByPageIds(Collections.singleton(pageId)); |
||||||
|
for (OnlineForm onlineForm : onlineFormListByPageIds) { |
||||||
|
List<OnlineDatasource> datasourceList = onlineDatasourceService.getOnlineDatasourceListByFormIds(Collections.singleton(onlineForm.getFormId())); |
||||||
|
Set<Long> datasourceIdSet = datasourceList.stream() |
||||||
|
.map(OnlineDatasource::getDatasourceId) |
||||||
|
.collect(Collectors.toSet()); |
||||||
|
RBucket<OnlineForm> bucket = redissonClient.getBucket("lastUpdateOnlineForm:" + onlineForm.getFormId()); |
||||||
|
OnlineForm lastUpdateOnlineForm; |
||||||
|
if (null != bucket) { |
||||||
|
lastUpdateOnlineForm = bucket.get(); |
||||||
|
} else { |
||||||
|
lastUpdateOnlineForm = null; |
||||||
|
} |
||||||
|
// 根据表单json中的relativeFormCode获取到对应的查询数据源数据
|
||||||
|
String widgetJson = onlineForm.getWidgetJson(); |
||||||
|
List<String> relativeFormCodeList = JsonPath.read(widgetJson, "$..relativeFormCode"); |
||||||
|
List<OnlCgreportHead> onlCgreportHeadList = iOnlCgreportHeadService.list(new LambdaQueryWrapper<OnlCgreportHead>() |
||||||
|
.in(OnlCgreportHead::getCode, relativeFormCodeList)); |
||||||
|
List<String> onlCgreportHeadIdList = onlCgreportHeadList.stream().map(OnlCgreportHead::getId).collect(Collectors.toList()); |
||||||
|
List<OnlCgreportItem> onlCgreportItemList = iOnlCgreportItemService.list(new LambdaQueryWrapper<OnlCgreportItem>() |
||||||
|
.in(OnlCgreportItem::getCgrheadId, onlCgreportHeadIdList)); |
||||||
|
List<OnlCgreportParam> onlCgreportParamList = iOnlCgreportParamService.list(new LambdaQueryWrapper<OnlCgreportParam>() |
||||||
|
.in(OnlCgreportParam::getCgrheadId, onlCgreportHeadIdList)); |
||||||
|
for (Long tenantId : tenantIdSet) { |
||||||
|
// 表单更新同步通知
|
||||||
|
JSONObject formMsgJsonData = new JSONObject(); |
||||||
|
formMsgJsonData.put("adminOnlineForm", onlineForm); |
||||||
|
formMsgJsonData.put("originalOnlineForm", lastUpdateOnlineForm); |
||||||
|
formMsgJsonData.put("datasourceIdSet", datasourceIdSet); |
||||||
|
dataSyncProducer.sendOrderly( |
||||||
|
tenantId, |
||||||
|
applicationConfig.getTenantSyncTopic(), |
||||||
|
OnlineForm.class.getSimpleName(), |
||||||
|
TenantConstant.UPDATE_TENANT_ONLINE_FORM_COMMAND, |
||||||
|
formMsgJsonData.toJSONString(), |
||||||
|
TenantConstant.MESSAGE_QUEUE_SELECTOR_KEY); |
||||||
|
//表单数据源更新同步新增
|
||||||
|
JSONObject onlCgreportHeadMsgJsonData = new JSONObject(); |
||||||
|
onlCgreportHeadMsgJsonData.put("onlCgreportHeadList", onlCgreportHeadList); |
||||||
|
onlCgreportHeadMsgJsonData.put("onlCgreportItemList", onlCgreportItemList); |
||||||
|
onlCgreportHeadMsgJsonData.put("onlCgreportParamList", onlCgreportParamList); |
||||||
|
dataSyncProducer.sendOrderly( |
||||||
|
tenantId, |
||||||
|
applicationConfig.getTenantSyncTopic(), |
||||||
|
OnlCgreportHead.class.getSimpleName(), |
||||||
|
TenantConstant.INSERT_TENANT_ONL_CGREPORT_HEAD_COMMAND, |
||||||
|
onlCgreportHeadMsgJsonData.toJSONString(), |
||||||
|
TenantConstant.MESSAGE_QUEUE_SELECTOR_KEY); |
||||||
|
} |
||||||
|
} |
||||||
|
//根据页面ID获取表单元数据列表
|
||||||
|
String sql = getOnlFormHeadByPageIdSQL(pageId); |
||||||
|
List<OnlFormHead> headList = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(OnlFormHead.class), Collections.singleton(pageId)); |
||||||
|
List<OnlFormFind> findList = onlFormFindService.list(new LambdaQueryWrapper<OnlFormFind>().in(OnlFormFind::getHeadId, headList)); |
||||||
|
List<OnlFormField> fieldList = onlFormFieldService.list(new LambdaQueryWrapper<OnlFormField>().in(OnlFormField::getHeadId, headList)); |
||||||
|
List<OnlFormCheck> checkList = onlFormCheckService.list(new LambdaQueryWrapper<OnlFormCheck>().in(OnlFormCheck::getHeadId, headList)); |
||||||
|
List<OnlFormIndex> indexList = onlFormIndexService.list(new LambdaQueryWrapper<OnlFormIndex>().in(OnlFormIndex::getHeadId, headList)); |
||||||
|
List<OnlFormKey> keyList = onlFormKeyService.list(new LambdaQueryWrapper<OnlFormKey>().in(OnlFormKey::getHeadId, headList)); |
||||||
|
List<OnlFormPage> pageList = onlFormPageService.list(new LambdaQueryWrapper<OnlFormPage>().in(OnlFormPage::getHeadId, headList)); |
||||||
|
// 表单元数据通知更新新增
|
||||||
|
for(Long tenantId : tenantIdSet){ |
||||||
|
JSONObject messageJsonData = new JSONObject(); |
||||||
|
messageJsonData.put("onlFormHeadList", headList); |
||||||
|
messageJsonData.put("onlFormFindList", findList); |
||||||
|
messageJsonData.put("onlFormFieldList", fieldList); |
||||||
|
messageJsonData.put("onlFormCheckList", checkList); |
||||||
|
messageJsonData.put("onlFormIndexList", indexList); |
||||||
|
messageJsonData.put("onlFormKeyList", keyList); |
||||||
|
messageJsonData.put("onlFormPageList", pageList); |
||||||
|
dataSyncProducer.sendOrderly( |
||||||
|
tenantId, |
||||||
|
applicationConfig.getTenantSyncTopic(), |
||||||
|
OnlFormHead.class.getSimpleName(), |
||||||
|
TenantConstant.INSERT_TENANT_ONL_FORM_HEAD_COMMAND, |
||||||
|
messageJsonData.toJSONString(), |
||||||
|
TenantConstant.MESSAGE_QUEUE_SELECTOR_KEY |
||||||
|
); |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
private static @NotNull String getOnlFormHeadByPageIdSQL(Long pageId) { |
||||||
|
String placeholders = pageId.toString(); |
||||||
|
String sql = "WITH RECURSIVE form_tree AS (" + |
||||||
|
"SELECT fh.* FROM onl_form_head fh " + |
||||||
|
"LEFT JOIN zz_online_page p ON p.master_table_id = fh.id " + |
||||||
|
"WHERE p.page_id IN (" + placeholders + ") " + |
||||||
|
"UNION ALL " + |
||||||
|
"SELECT child.* FROM onl_form_head child " + |
||||||
|
"INNER JOIN form_tree parent ON child.sort = parent.table_name " + |
||||||
|
"WHERE child.sort IS NOT NULL AND child.sort != ''" + |
||||||
|
") SELECT DISTINCT * FROM form_tree"; |
||||||
|
return sql; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,95 @@ |
|||||||
|
package apelet.tenantsync.online.service; |
||||||
|
|
||||||
|
import apelet.common.datasync.base.service.BaseDataSyncConsumerService; |
||||||
|
import apelet.common.datasync.consumer.BaseDataSyncRocketMqConsumer; |
||||||
|
import apelet.common.generator.model.*; |
||||||
|
import apelet.common.generator.service.IOnlCgreportHeadService; |
||||||
|
import apelet.common.generator.service.IOnlCgreportItemService; |
||||||
|
import apelet.common.generator.service.IOnlCgreportParamService; |
||||||
|
import apelet.common.tenant.constant.TenantConstant; |
||||||
|
import apelet.tenantsync.consumer.TenantSyncConsumer; |
||||||
|
import cn.hutool.core.util.StrUtil; |
||||||
|
import com.alibaba.fastjson.JSONObject; |
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
||||||
|
import lombok.extern.slf4j.Slf4j; |
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.stereotype.Service; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
|
||||||
|
@Slf4j |
||||||
|
@Service |
||||||
|
public class SysTenantOnlCgreporHeadServiceImpl implements BaseDataSyncConsumerService { |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private IOnlCgreportHeadService iOnlCgreportHeadService; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private IOnlCgreportItemService iOnlCgreportItemService; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private IOnlCgreportParamService iOnlCgreportParamService; |
||||||
|
|
||||||
|
@Override |
||||||
|
public void doHandle(String transId, String messageCommand, JSONObject messageJsonData) { |
||||||
|
if (StrUtil.equals(messageCommand, TenantConstant.INSERT_TENANT_ONL_FORM_HEAD_COMMAND)) { |
||||||
|
List<OnlCgreportHead> onlCgreportHeadList = messageJsonData.getJSONArray("onlCgreportHeadList").toJavaList(OnlCgreportHead.class); |
||||||
|
List<OnlCgreportItem> onlCgreportItemList = messageJsonData.getJSONArray("onlCgreportItemList").toJavaList(OnlCgreportItem.class); |
||||||
|
List<OnlCgreportParam> onlCgreportParamList = messageJsonData.getJSONArray("onlCgreportParamList").toJavaList(OnlCgreportParam.class); |
||||||
|
//对需要同步的查询数据源数据进行多租户处理
|
||||||
|
onlCgreportHeadList.stream().map(onlCgreportHead -> { |
||||||
|
onlCgreportHead.setTenantId(transId); |
||||||
|
return onlCgreportHead; |
||||||
|
}).collect(Collectors.toList()); |
||||||
|
for (OnlCgreportHead onlCgreportHead : onlCgreportHeadList) { |
||||||
|
//判断对应租户是否已经存在相同名称的查询数据源
|
||||||
|
OnlCgreportHead one = iOnlCgreportHeadService.getOne(new LambdaQueryWrapper<OnlCgreportHead>() |
||||||
|
.eq(OnlCgreportHead::getCode, onlCgreportHead.getCode()) |
||||||
|
.eq(OnlCgreportHead::getTenantId, transId)); |
||||||
|
if (one == null) { |
||||||
|
// 把onlCgreportItemList、onlCgreportParamList中headId和onlCgreportHead的id匹配的数据过滤出来
|
||||||
|
List<OnlCgreportItem> matchedItemList = onlCgreportItemList.stream() |
||||||
|
.filter(item -> item.getCgrheadId() != null && item.getCgrheadId().equals(onlCgreportHead.getId())) |
||||||
|
.collect(Collectors.toList()); |
||||||
|
List<OnlCgreportParam> matchedParamList = onlCgreportParamList.stream() |
||||||
|
.filter(param -> param.getCgrheadId() != null && param.getCgrheadId().equals(onlCgreportHead.getId())) |
||||||
|
.collect(Collectors.toList()); |
||||||
|
// 把onlFormHead保存进数据库,并获取保存后的id
|
||||||
|
onlCgreportHead.setId(null); |
||||||
|
iOnlCgreportHeadService.save(onlCgreportHead); |
||||||
|
String newId = onlCgreportHead.getId(); |
||||||
|
// 将获取到的onlCgreportHead的id赋值到过滤匹配出来的数据的cgrheadId上,并将数据保存进数据库
|
||||||
|
for (OnlCgreportItem onlCgreportItem : matchedItemList) { |
||||||
|
onlCgreportItem.setId(null); |
||||||
|
onlCgreportItem.setCgrheadId(newId); |
||||||
|
} |
||||||
|
iOnlCgreportItemService.saveBatch(matchedItemList); |
||||||
|
for (OnlCgreportParam onlCgreportParam : matchedParamList) { |
||||||
|
onlCgreportParam.setId(null); |
||||||
|
onlCgreportParam.setCgrheadId(newId); |
||||||
|
} |
||||||
|
iOnlCgreportParamService.saveBatch(matchedParamList); |
||||||
|
} |
||||||
|
} |
||||||
|
} else if (StrUtil.equals(messageCommand, TenantConstant.DELETE_TENANT_ONL_FORM_HEAD_COMMAND)) { |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String handleMessageType() { |
||||||
|
return OnlCgreportHead.class.getSimpleName(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 返回关联的RocketMQ消费者服务。 |
||||||
|
* 该服务的实现类,将只是处理来自该消费者的消息。 |
||||||
|
* |
||||||
|
* @return 关联的RocketMQ消费者服务。 |
||||||
|
*/ |
||||||
|
@Override |
||||||
|
public Class<? extends BaseDataSyncRocketMqConsumer> handleConsumerClass() { |
||||||
|
return TenantSyncConsumer.class; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,139 @@ |
|||||||
|
package apelet.tenantsync.online.service; |
||||||
|
|
||||||
|
import apelet.common.datasync.base.service.BaseDataSyncConsumerService; |
||||||
|
import apelet.common.datasync.consumer.BaseDataSyncRocketMqConsumer; |
||||||
|
import apelet.common.generator.model.*; |
||||||
|
import apelet.common.generator.service.*; |
||||||
|
import apelet.common.tenant.constant.TenantConstant; |
||||||
|
import apelet.tenantsync.consumer.TenantSyncConsumer; |
||||||
|
import cn.hutool.core.util.StrUtil; |
||||||
|
import com.alibaba.fastjson.JSONObject; |
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
||||||
|
import lombok.extern.slf4j.Slf4j; |
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.stereotype.Service; |
||||||
|
import org.w3c.dom.stylesheets.LinkStyle; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
|
||||||
|
@Slf4j |
||||||
|
@Service |
||||||
|
public class SysTenantOnlFormHeadServiceImpl implements BaseDataSyncConsumerService { |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private IOnlFormHeadService onlFormHeadService; |
||||||
|
@Autowired |
||||||
|
private IOnlFormFindService onlFormFindService; |
||||||
|
@Autowired |
||||||
|
private IOnlFormFieldService onlFormFieldService; |
||||||
|
@Autowired |
||||||
|
private IOnlFormCheckService onlFormCheckService; |
||||||
|
@Autowired |
||||||
|
private IOnlFormIndexService onlFormIndexService; |
||||||
|
@Autowired |
||||||
|
private IOnlFormKeyService onlFormKeyService; |
||||||
|
@Autowired |
||||||
|
private IOnlFormPageService onlFormPageService; |
||||||
|
|
||||||
|
@Override |
||||||
|
public void doHandle(String transId, String messageCommand, JSONObject messageJsonData) { |
||||||
|
if (StrUtil.equals(messageCommand, TenantConstant.INSERT_TENANT_ONL_FORM_HEAD_COMMAND)) { |
||||||
|
List<OnlFormHead> onlFormHeadList = messageJsonData.getJSONArray("onlFormHeadList").toJavaList(OnlFormHead.class); |
||||||
|
List<OnlFormFind> onlFormFindList = messageJsonData.getJSONArray("onlFormFindList").toJavaList(OnlFormFind.class); |
||||||
|
List<OnlFormField> onlFormFieldList = messageJsonData.getJSONArray("onlFormFieldList").toJavaList(OnlFormField.class); |
||||||
|
List<OnlFormCheck> onlFormCheckList = messageJsonData.getJSONArray("onlFormCheckList").toJavaList(OnlFormCheck.class); |
||||||
|
List<OnlFormIndex> onlFormIndexList = messageJsonData.getJSONArray("onlFormIndexList").toJavaList(OnlFormIndex.class); |
||||||
|
List<OnlFormKey> onlFormKeyList = messageJsonData.getJSONArray("onlFormKeyList").toJavaList(OnlFormKey.class); |
||||||
|
List<OnlFormPage> onlFormPageList = messageJsonData.getJSONArray("onlFormPageList").toJavaList(OnlFormPage.class); |
||||||
|
//对需要同步的表单元数据进行多租户处理
|
||||||
|
onlFormHeadList.stream().map(onlFormHead -> { |
||||||
|
onlFormHead.setTenantId(transId); |
||||||
|
onlFormHead.setTableName(onlFormHead.getTableName() + "_" + transId); |
||||||
|
return onlFormHead; |
||||||
|
}).collect(Collectors.toList()); |
||||||
|
for (OnlFormHead onlFormHead : onlFormHeadList) { |
||||||
|
//判断对应租户是否已经存在相同名称的表单元数据
|
||||||
|
OnlFormHead one = onlFormHeadService.getOne(new LambdaQueryWrapper<OnlFormHead>() |
||||||
|
.eq(OnlFormHead::getTableName, onlFormHead.getTableName())); |
||||||
|
if (one == null) { |
||||||
|
// 把onlFormFindList、onlFormFieldList、onlFormCheckList、onlFormIndexList、onlFormKeyList、onlFormPageList
|
||||||
|
// 中headId和onlFormHead的id匹配的数据过滤出来
|
||||||
|
List<OnlFormFind> matchedFindList = onlFormFindList.stream() |
||||||
|
.filter(find -> find.getHeadId() != null && find.getHeadId().equals(onlFormHead.getId())) |
||||||
|
.collect(Collectors.toList()); |
||||||
|
List<OnlFormField> matchedFieldList = onlFormFieldList.stream() |
||||||
|
.filter(field -> field.getHeadId() != null && field.getHeadId().equals(onlFormHead.getId())) |
||||||
|
.collect(Collectors.toList()); |
||||||
|
List<OnlFormCheck> matchedCheckList = onlFormCheckList.stream() |
||||||
|
.filter(check -> check.getHeadId() != null && check.getHeadId().equals(onlFormHead.getId())) |
||||||
|
.collect(Collectors.toList()); |
||||||
|
List<OnlFormIndex> matchedIndexList = onlFormIndexList.stream() |
||||||
|
.filter(index -> index.getHeadId() != null && index.getHeadId().equals(onlFormHead.getId())) |
||||||
|
.collect(Collectors.toList()); |
||||||
|
List<OnlFormKey> matchedKeyList = onlFormKeyList.stream() |
||||||
|
.filter(key -> key.getHeadId() != null && key.getHeadId().equals(onlFormHead.getId())) |
||||||
|
.collect(Collectors.toList()); |
||||||
|
List<OnlFormPage> matchedPageList = onlFormPageList.stream() |
||||||
|
.filter(page -> page.getHeadId() != null && page.getHeadId().equals(onlFormHead.getId())) |
||||||
|
.collect(Collectors.toList()); |
||||||
|
// 把onlFormHead保存进数据库,并获取保存后的id
|
||||||
|
onlFormHead.setId(null); |
||||||
|
onlFormHeadService.save(onlFormHead); |
||||||
|
Long newId = onlFormHead.getId(); |
||||||
|
// 将获取到的onlFormHead的id赋值到过滤匹配出来的数据的headId上,并将数据保存进数据库
|
||||||
|
for (OnlFormFind onlFormFind : matchedFindList) { |
||||||
|
onlFormFind.setId(null); |
||||||
|
onlFormFind.setHeadId(newId); |
||||||
|
} |
||||||
|
onlFormFindService.saveBatch(matchedFindList); |
||||||
|
for (OnlFormField onlFormField : matchedFieldList) { |
||||||
|
onlFormField.setId(null); |
||||||
|
onlFormField.setHeadId(newId); |
||||||
|
} |
||||||
|
onlFormFieldService.saveBatch(matchedFieldList); |
||||||
|
for (OnlFormCheck onlFormCheck : matchedCheckList) { |
||||||
|
onlFormCheck.setId(null); |
||||||
|
onlFormCheck.setHeadId(newId); |
||||||
|
} |
||||||
|
onlFormCheckService.saveBatch(matchedCheckList); |
||||||
|
for (OnlFormIndex onlFormIndex : matchedIndexList) { |
||||||
|
onlFormIndex.setId(null); |
||||||
|
onlFormIndex.setHeadId(newId); |
||||||
|
} |
||||||
|
onlFormIndexService.saveBatch(matchedIndexList); |
||||||
|
for (OnlFormKey onlFormKey : matchedKeyList) { |
||||||
|
onlFormKey.setId(null); |
||||||
|
onlFormKey.setHeadId(newId); |
||||||
|
} |
||||||
|
onlFormKeyService.saveBatch(matchedKeyList); |
||||||
|
for (OnlFormPage onlFormPage : matchedPageList) { |
||||||
|
onlFormPage.setId(null); |
||||||
|
onlFormPage.setHeadId(newId); |
||||||
|
} |
||||||
|
onlFormPageService.saveBatch(matchedPageList); |
||||||
|
//表单元数据同步
|
||||||
|
onlFormHeadService.syncDB(onlFormHead, 0L); |
||||||
|
} |
||||||
|
} |
||||||
|
} else if (StrUtil.equals(messageCommand, TenantConstant.DELETE_TENANT_ONL_FORM_HEAD_COMMAND)) { |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String handleMessageType() { |
||||||
|
return OnlFormHead.class.getSimpleName(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 返回关联的RocketMQ消费者服务。 |
||||||
|
* 该服务的实现类,将只是处理来自该消费者的消息。 |
||||||
|
* |
||||||
|
* @return 关联的RocketMQ消费者服务。 |
||||||
|
*/ |
||||||
|
@Override |
||||||
|
public Class<? extends BaseDataSyncRocketMqConsumer> handleConsumerClass() { |
||||||
|
return TenantSyncConsumer.class; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,88 @@ |
|||||||
|
package apelet.tenantsync.online.service; |
||||||
|
|
||||||
|
import apelet.common.datasync.base.service.BaseDataSyncConsumerService; |
||||||
|
import apelet.common.datasync.consumer.BaseDataSyncRocketMqConsumer; |
||||||
|
import apelet.common.online.dao.OnlineFormDatasourceMapper; |
||||||
|
import apelet.common.online.dao.OnlineFormMapper; |
||||||
|
import apelet.common.online.model.OnlineForm; |
||||||
|
import apelet.common.online.model.OnlineFormDatasource; |
||||||
|
import apelet.common.sequence.wrapper.IdGeneratorWrapper; |
||||||
|
import apelet.common.tenant.constant.TenantConstant; |
||||||
|
import apelet.tenantsync.consumer.TenantSyncConsumer; |
||||||
|
import apelet.tenantsync.online.utils.FormWidgetMergeUtils; |
||||||
|
import cn.hutool.core.collection.CollUtil; |
||||||
|
import cn.hutool.core.util.StrUtil; |
||||||
|
import com.alibaba.fastjson.JSON; |
||||||
|
import com.alibaba.fastjson.JSONObject; |
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
||||||
|
import lombok.extern.slf4j.Slf4j; |
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.stereotype.Service; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
@Slf4j |
||||||
|
@Service |
||||||
|
public class SysTenantOnlineFormUpdateServiceImpl implements BaseDataSyncConsumerService { |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private IdGeneratorWrapper idGenerator; |
||||||
|
@Autowired |
||||||
|
private OnlineFormMapper onlineFormMapper; |
||||||
|
@Autowired |
||||||
|
private OnlineFormDatasourceMapper onlineFormDatasourceMapper; |
||||||
|
|
||||||
|
@Override |
||||||
|
public void doHandle(String transId, String messageCommand, JSONObject messageJsonData) { |
||||||
|
if (StrUtil.equals(messageCommand, TenantConstant.UPDATE_TENANT_ONLINE_FORM_COMMAND)) { |
||||||
|
OnlineForm onlineForm = messageJsonData.getJSONObject("adminOnlineForm").toJavaObject(OnlineForm.class); |
||||||
|
OnlineForm originalOnlineForm = messageJsonData.getJSONObject("originalOnlineForm").toJavaObject(OnlineForm.class); |
||||||
|
List<Long> datasourceIdSet = messageJsonData.getJSONArray("datasourceIdSet").toJavaList(Long.class); |
||||||
|
if (originalOnlineForm == null) { |
||||||
|
originalOnlineForm = onlineForm; |
||||||
|
} |
||||||
|
//获取租户数据库中所有对应form_code的表单数据
|
||||||
|
List<OnlineForm> dbOnlineFormList = onlineFormMapper.selectList(new QueryWrapper<OnlineForm>().eq("form_code", onlineForm.getFormCode())); |
||||||
|
for (OnlineForm dbOnlineForm : dbOnlineFormList) { |
||||||
|
//三路合并表单json
|
||||||
|
String mergeFormJson = FormWidgetMergeUtils.threeWayMerge( |
||||||
|
JSON.parseObject(originalOnlineForm.getWidgetJson()) |
||||||
|
, JSON.parseObject(dbOnlineForm.getWidgetJson()) |
||||||
|
, JSON.parseObject(onlineForm.getWidgetJson())).toJSONString(); |
||||||
|
dbOnlineForm.setWidgetJson(mergeFormJson); |
||||||
|
//更新租户数据库中的表单json数据
|
||||||
|
onlineFormMapper.update(dbOnlineForm, new QueryWrapper<OnlineForm>().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; |
||||||
|
} |
||||||
|
} |
||||||
@ -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 深度合并工具 |
||||||
|
* <p> |
||||||
|
* 合并策略: |
||||||
|
* <ul> |
||||||
|
* <li>对象(JSONObject):递归深度合并,source 覆盖 target 的同名键</li> |
||||||
|
* <li>数组(JSONArray):按业务键去重合并,具体逻辑由数组合并策略决定</li> |
||||||
|
* <li>标量值:source 直接覆盖 target</li> |
||||||
|
* </ul> |
||||||
|
* <p> |
||||||
|
*/ |
||||||
|
public class FormWidgetMergeUtils { |
||||||
|
|
||||||
|
/** |
||||||
|
* 数组合并策略枚举 |
||||||
|
*/ |
||||||
|
public enum ArrayMergeStrategy { |
||||||
|
/** 按业务键去重合并:相同键的元素由 source 覆盖 target,新增元素追加 */ |
||||||
|
MERGE_BY_KEY, |
||||||
|
/** 直接追加:source 数组元素全部追加到 target 数组末尾 */ |
||||||
|
APPEND |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 数组路径 → 业务键 映射配置 |
||||||
|
* <p> |
||||||
|
* key 为 JSON 路径中数组字段的名称,value 为该数组中元素的业务主键字段名 |
||||||
|
* 例如 "operationList" → "type" 表示 operationList 数组按 type 字段去重 |
||||||
|
*/ |
||||||
|
private static final Map<String, String> ARRAY_BUSINESS_KEY_MAP = new LinkedHashMap<>(); |
||||||
|
|
||||||
|
/** |
||||||
|
* 数组路径 → 合并策略 映射配置 |
||||||
|
* <p> |
||||||
|
* 未在此映射中配置的数组字段,默认使用 APPEND 策略 |
||||||
|
*/ |
||||||
|
private static final Map<String, ArrayMergeStrategy> 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 的修改 |
||||||
|
* <p> |
||||||
|
* 合并逻辑: |
||||||
|
* 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 的差异 |
||||||
|
* <p> |
||||||
|
* 返回一个包含所有新增和修改字段的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<String, Object> 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<String, Integer> baseKeyIndexMap = buildKeyIndexMap(baseArray, businessKey); |
||||||
|
Map<String, Integer> sourceKeyIndexMap = buildKeyIndexMap(sourceArray, businessKey); |
||||||
|
|
||||||
|
// 检查 source 中新增或修改的元素
|
||||||
|
for (Map.Entry<String, Integer> 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<String, Object> 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<String, Integer> 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; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 应用差异但不覆盖已修改的部分 |
||||||
|
* <p> |
||||||
|
* 这是三路合并的核心逻辑:确保 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<String, Object> 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<String, Integer> targetKeyIndexMap = buildKeyIndexMap(result, businessKey); |
||||||
|
// 构建 diff1 的业务键到元素的映射(而不是索引映射)
|
||||||
|
Map<String, JSONObject> 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<String, JSONObject> buildKeyElementMap(JSONArray array, String businessKey) { |
||||||
|
Map<String, JSONObject> 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<String, Object> 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); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 按业务键去重合并数组 |
||||||
|
* <p> |
||||||
|
* 逻辑: |
||||||
|
* 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<String, Integer> 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 元素的 业务键值 → 索引 映射 |
||||||
|
* <p> |
||||||
|
* 仅处理 JSONObject 类型的元素;标量元素跳过。 |
||||||
|
* 若同一业务键值出现多次,后出现的索引覆盖先前的(保留最后一个)。 |
||||||
|
* |
||||||
|
* @param array JSON数组 |
||||||
|
* @param businessKey 业务主键字段名 |
||||||
|
* @return 业务键值字符串 → 在数组中的索引位置 |
||||||
|
*/ |
||||||
|
private static Map<String, Integer> buildKeyIndexMap(JSONArray array, String businessKey) { |
||||||
|
Map<String, Integer> 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 中指定业务键字段的值,统一转为字符串用于比较 |
||||||
|
* <p> |
||||||
|
* 支持的值类型: |
||||||
|
* <ul> |
||||||
|
* <li>String → 直接返回</li> |
||||||
|
* <li>Number → 返回其字符串表示(如 1 → "1", 10 → "10")</li> |
||||||
|
* <li>Boolean → 返回 "true" / "false"</li> |
||||||
|
* <li>null 或字段不存在 → 返回 null</li> |
||||||
|
* </ul> |
||||||
|
* |
||||||
|
* @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<byte[]> 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); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
package apelet.common.tenant.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.*; |
||||||
|
|
||||||
|
/** |
||||||
|
* 用于标记需要在 datasourceVariableName 参数后拼接租户ID后缀的注解。 |
||||||
|
* |
||||||
|
* @author guifc |
||||||
|
* @date 2026-05-19 |
||||||
|
*/ |
||||||
|
@Target({ElementType.TYPE, ElementType.METHOD}) |
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Documented |
||||||
|
public @interface TenantOnlFormHeadSuffix { |
||||||
|
|
||||||
|
/** |
||||||
|
* 是否启用租户ID后缀拼接。 |
||||||
|
* |
||||||
|
* @return 是否启用,默认启用。 |
||||||
|
*/ |
||||||
|
boolean enabled() default true; |
||||||
|
} |
||||||
@ -0,0 +1,97 @@ |
|||||||
|
package apelet.common.tenant.aop; |
||||||
|
|
||||||
|
import apelet.common.core.object.TokenData; |
||||||
|
import apelet.common.tenant.annotation.TenantOnlFormHeadSuffix; |
||||||
|
import cn.hutool.core.util.StrUtil; |
||||||
|
import lombok.extern.slf4j.Slf4j; |
||||||
|
import org.aspectj.lang.JoinPoint; |
||||||
|
import org.aspectj.lang.ProceedingJoinPoint; |
||||||
|
import org.aspectj.lang.Signature; |
||||||
|
import org.aspectj.lang.annotation.Around; |
||||||
|
import org.aspectj.lang.annotation.Aspect; |
||||||
|
import org.aspectj.lang.annotation.Pointcut; |
||||||
|
import org.aspectj.lang.reflect.MethodSignature; |
||||||
|
import org.springframework.core.annotation.Order; |
||||||
|
import org.springframework.stereotype.Component; |
||||||
|
|
||||||
|
import java.lang.reflect.Method; |
||||||
|
|
||||||
|
/** |
||||||
|
* 织入TenantOnlFormHeadSuffix注解,处理接口中datasourceVariableName参数的租户ID后缀拼接。 |
||||||
|
* |
||||||
|
* @author guifc |
||||||
|
* @date 2026-05-19 |
||||||
|
*/ |
||||||
|
@Aspect |
||||||
|
@Component |
||||||
|
@Order(2) |
||||||
|
@Slf4j |
||||||
|
public class TenantOnlFormHeadSuffixHandlerAspect { |
||||||
|
|
||||||
|
@Pointcut("@within(apelet.common.tenant.annotation.TenantOnlFormHeadSuffix) " + |
||||||
|
"|| @annotation(apelet.common.tenant.annotation.TenantOnlFormHeadSuffix)") |
||||||
|
public void tenantOnlFormHeadSuffixHandlerPointCut() { |
||||||
|
// 空注释,避免sonar警告
|
||||||
|
} |
||||||
|
|
||||||
|
@Around("tenantOnlFormHeadSuffixHandlerPointCut()") |
||||||
|
public Object around(ProceedingJoinPoint point) throws Throwable { |
||||||
|
TenantOnlFormHeadSuffix annotation = this.getAnnotation(point); |
||||||
|
if (annotation == null || !annotation.enabled()) { |
||||||
|
return point.proceed(); |
||||||
|
} |
||||||
|
|
||||||
|
// 获取方法签名
|
||||||
|
MethodSignature signature = (MethodSignature) point.getSignature(); |
||||||
|
Method method = signature.getMethod(); |
||||||
|
String[] parameterNames = signature.getParameterNames(); |
||||||
|
Object[] args = point.getArgs(); |
||||||
|
|
||||||
|
// 从token中获取租户ID
|
||||||
|
TokenData tokenData = TokenData.takeFromRequest(); |
||||||
|
if (tokenData == null || tokenData.getTenantId() == null) { |
||||||
|
log.warn("TenantId is null in TokenData, skip datasourceVariableName suffix handling."); |
||||||
|
return point.proceed(); |
||||||
|
} |
||||||
|
|
||||||
|
Long tenantId = tokenData.getTenantId(); |
||||||
|
String suffix = "_" + tenantId; |
||||||
|
|
||||||
|
// 遍历参数,查找并修改datasourceVariableName参数
|
||||||
|
for (int i = 0; i < parameterNames.length; i++) { |
||||||
|
if ("datasourceVariableName".equals(parameterNames[i]) && args[i] instanceof String) { |
||||||
|
String originalValue = (String) args[i]; |
||||||
|
if (StrUtil.isNotBlank(originalValue) && !originalValue.endsWith(suffix)) { |
||||||
|
String newValue = originalValue + suffix; |
||||||
|
args[i] = newValue; |
||||||
|
log.debug("Modified datasourceVariableName from [{}] to [{}] for tenant [{}]", |
||||||
|
originalValue, newValue, tenantId); |
||||||
|
} |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
return point.proceed(args); |
||||||
|
} catch (Exception e) { |
||||||
|
log.error("Error in TenantOnlFormHeadSuffixHandlerAspect: {}", e.getMessage(), e); |
||||||
|
throw e; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private TenantOnlFormHeadSuffix getAnnotation(JoinPoint joinPoint) { |
||||||
|
Signature signature = joinPoint.getSignature(); |
||||||
|
MethodSignature methodSignature = (MethodSignature) signature; |
||||||
|
|
||||||
|
// 先尝试从方法上获取注解
|
||||||
|
TenantOnlFormHeadSuffix anno = methodSignature.getMethod() |
||||||
|
.getAnnotation(TenantOnlFormHeadSuffix.class); |
||||||
|
if (anno != null) { |
||||||
|
return anno; |
||||||
|
} |
||||||
|
|
||||||
|
// 再从类上获取注解
|
||||||
|
Class<?> declaringType = methodSignature.getDeclaringType(); |
||||||
|
return declaringType.getAnnotation(TenantOnlFormHeadSuffix.class); |
||||||
|
} |
||||||
|
} |
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in new issue