From 8bf1d4638cedea09b2c022990389352336577a10 Mon Sep 17 00:00:00 2001 From: chenchuchuan <13554600537@163.com> Date: Thu, 29 Oct 2026 14:00:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(tenant):=20=E5=AE=9E=E7=8E=B0=E7=A7=9F?= =?UTF-8?q?=E6=88=B7=E8=87=AA=E5=AE=9A=E4=B9=89=E5=AD=97=E6=AE=B5tenant=5F?= =?UTF-8?q?=E5=89=8D=E7=BC=80=E9=9A=94=E7=A6=BB=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 TenantFieldUtil 工具类,定义 tenant_ 前缀命名约定 - 管理端新增/编辑字段时校验禁止占用 tenant_ 前缀 - 租户端新增字段自动追加 tenant_ 前缀并同步改写 widgetJson 列名 - 拆分 OnlineFormTenantSyncService 为 syncFormUpdate 和 syncFormMetadata - SysTenantRoleServiceImpl 移除内联同步逻辑,委托统一服务调用 - 新增 SyncLog 模型、Mapper 及 SyncLogService 记录同步操作日志 - 升级 common-online 至 1.0.7、common-generator 至 1.0.6 --- application-tenant/tenant-admin/pom.xml | 2 +- .../service/OnlineFormTenantSyncService.java | 11 ++ .../impl/OnlineFormTenantSyncServiceImpl.java | 147 +++++++++++--------- .../service/impl/SysTenantRoleServiceImpl.java | 134 +----------------- application-tenant/tenant-sync/pom.xml | 2 +- .../aop/DynamicTenantDatasourceAspect.java | 69 +++++++++- .../SysTenantOnlCgreporHeadServiceImpl.java | 3 + .../service/SysTenantOnlFormHeadServiceImpl.java | 22 ++- .../SysTenantOnlineFormUpdateServiceImpl.java | 132 ++++++++++++++---- application/upms/upms-service/pom.xml | 2 +- common/common-flow-online/pom.xml | 2 +- .../controller/OnlFormHeadController.java | 24 ++++ .../online/controller/OnlineFormController.java | 14 ++ .../apelet/common/online/util/TenantFieldUtil.java | 150 +++++++++++++++++++++ common/common-openapi/pom.xml | 4 +- common/common-tenant-online/pom.xml | 2 +- .../apelet/common/tenant/dao/SyncLogMapper.java | 12 ++ .../java/apelet/common/tenant/model/SyncLog.java | 55 ++++++++ .../common/tenant/service/SyncLogService.java | 79 +++++++++++ 19 files changed, 640 insertions(+), 226 deletions(-) create mode 100644 common/common-online/src/main/java/apelet/common/online/util/TenantFieldUtil.java create mode 100644 common/common-tenant/src/main/java/apelet/common/tenant/dao/SyncLogMapper.java create mode 100644 common/common-tenant/src/main/java/apelet/common/tenant/model/SyncLog.java create mode 100644 common/common-tenant/src/main/java/apelet/common/tenant/service/SyncLogService.java diff --git a/application-tenant/tenant-admin/pom.xml b/application-tenant/tenant-admin/pom.xml index 8e7f07a..5a4f489 100644 --- a/application-tenant/tenant-admin/pom.xml +++ b/application-tenant/tenant-admin/pom.xml @@ -104,7 +104,7 @@ apelet common-generator - 1.0.5 + 1.0.6 diff --git a/application-tenant/tenant-admin/src/main/java/apelet/tenantadmin/tenant/service/OnlineFormTenantSyncService.java b/application-tenant/tenant-admin/src/main/java/apelet/tenantadmin/tenant/service/OnlineFormTenantSyncService.java index 039b42d..e4ad95a 100644 --- a/application-tenant/tenant-admin/src/main/java/apelet/tenantadmin/tenant/service/OnlineFormTenantSyncService.java +++ b/application-tenant/tenant-admin/src/main/java/apelet/tenantadmin/tenant/service/OnlineFormTenantSyncService.java @@ -1,10 +1,21 @@ package apelet.tenantadmin.tenant.service; +import java.util.Collection; import java.util.List; public interface OnlineFormTenantSyncService { Boolean syncUpdateTenant(Long pageId, List tenantIdSet); + /** + * 只同步表单 widgetJson(新增控件只追加)。 + */ + void syncFormUpdate(Long pageId, Collection tenantIdSet); + + /** + * 同步表单元数据与查询数据源。 + */ + void syncFormMetadata(Collection pageIds, Collection tenantIdSet); + } diff --git a/application-tenant/tenant-admin/src/main/java/apelet/tenantadmin/tenant/service/impl/OnlineFormTenantSyncServiceImpl.java b/application-tenant/tenant-admin/src/main/java/apelet/tenantadmin/tenant/service/impl/OnlineFormTenantSyncServiceImpl.java index 2658865..f3b346b 100644 --- a/application-tenant/tenant-admin/src/main/java/apelet/tenantadmin/tenant/service/impl/OnlineFormTenantSyncServiceImpl.java +++ b/application-tenant/tenant-admin/src/main/java/apelet/tenantadmin/tenant/service/impl/OnlineFormTenantSyncServiceImpl.java @@ -16,6 +16,7 @@ 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 cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; @@ -27,6 +28,7 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Set; @@ -85,6 +87,16 @@ public class OnlineFormTenantSyncServiceImpl implements OnlineFormTenantSyncServ @Override @Transactional(rollbackFor = Exception.class) public Boolean syncUpdateTenant(Long pageId, List tenantIdSet) { + syncFormUpdate(pageId, tenantIdSet); + syncFormMetadata(Collections.singletonList(pageId), tenantIdSet); + return true; + } + + @Override + public void syncFormUpdate(Long pageId, Collection tenantIdSet) { + if (pageId == null || CollUtil.isEmpty(tenantIdSet)) { + return; + } List onlineFormListByPageIds = onlineFormService.getOnlineFormListByPageIds(Collections.singleton(pageId)); for (OnlineForm onlineForm : onlineFormListByPageIds) { List datasourceList = onlineDatasourceService.getOnlineDatasourceListByFormIds(Collections.singleton(onlineForm.getFormId())); @@ -92,30 +104,8 @@ public class OnlineFormTenantSyncServiceImpl implements OnlineFormTenantSyncServ .map(OnlineDatasource::getDatasourceId) .collect(Collectors.toSet()); RBucket bucket = redissonClient.getBucket("lastUpdateOnlineForm:" + onlineForm.getFormId()); - OnlineForm lastUpdateOnlineForm; - if (null != bucket) { - lastUpdateOnlineForm = bucket.get(); - } else { - lastUpdateOnlineForm = null; - } - // 根据表单json中的relativeFormCode获取到对应的查询数据源数据 - String widgetJson = onlineForm.getWidgetJson(); - List relativeFormCodeList = parseRelativeFormCodes(widgetJson); - List onlCgreportHeadList = relativeFormCodeList.isEmpty() - ? Collections.emptyList() - : iOnlCgreportHeadService.list(new LambdaQueryWrapper() - .in(OnlCgreportHead::getCode, relativeFormCodeList)); - List onlCgreportHeadIdList = onlCgreportHeadList.stream().map(OnlCgreportHead::getId).collect(Collectors.toList()); - List onlCgreportItemList = onlCgreportHeadIdList.isEmpty() - ? Collections.emptyList() - : iOnlCgreportItemService.list(new LambdaQueryWrapper() - .in(OnlCgreportItem::getCgrheadId, onlCgreportHeadIdList)); - List onlCgreportParamList = onlCgreportHeadIdList.isEmpty() - ? Collections.emptyList() - : iOnlCgreportParamService.list(new LambdaQueryWrapper() - .in(OnlCgreportParam::getCgrheadId, onlCgreportHeadIdList)); + OnlineForm lastUpdateOnlineForm = bucket.get(); for (Long tenantId : tenantIdSet) { - // 表单更新同步通知 JSONObject formMsgJsonData = new JSONObject(); formMsgJsonData.put("tenantId", tenantId); formMsgJsonData.put("adminOnlineForm", onlineForm); @@ -128,51 +118,84 @@ public class OnlineFormTenantSyncServiceImpl implements OnlineFormTenantSyncServ TenantConstant.UPDATE_TENANT_ONLINE_FORM_COMMAND, formMsgJsonData.toJSONString(), TenantConstant.MESSAGE_QUEUE_SELECTOR_KEY); - //表单数据源更新同步新增 - JSONObject onlCgreportHeadMsgJsonData = new JSONObject(); - onlCgreportHeadMsgJsonData.put("tenantId", tenantId); - onlCgreportHeadMsgJsonData.put("onlCgreportHeadList", onlCgreportHeadList); - onlCgreportHeadMsgJsonData.put("onlCgreportItemList", onlCgreportItemList); - onlCgreportHeadMsgJsonData.put("onlCgreportParamList", onlCgreportParamList); + } + } + } + + @Override + public void syncFormMetadata(Collection pageIds, Collection tenantIdSet) { + if (CollUtil.isEmpty(pageIds) || CollUtil.isEmpty(tenantIdSet)) { + return; + } + // 1. 查询数据源:按表单 widgetJson 里的 relativeFormCode 反查 + for (Long pageId : pageIds) { + List onlineFormList = onlineFormService.getOnlineFormListByPageIds(Collections.singleton(pageId)); + for (OnlineForm onlineForm : onlineFormList) { + List relativeFormCodeList = parseRelativeFormCodes(onlineForm.getWidgetJson()); + List onlCgreportHeadList = relativeFormCodeList.isEmpty() + ? Collections.emptyList() + : iOnlCgreportHeadService.list(new LambdaQueryWrapper() + .in(OnlCgreportHead::getCode, relativeFormCodeList)); + List onlCgreportHeadIdList = onlCgreportHeadList.stream().map(OnlCgreportHead::getId).collect(Collectors.toList()); + List onlCgreportItemList = onlCgreportHeadIdList.isEmpty() + ? Collections.emptyList() + : iOnlCgreportItemService.list(new LambdaQueryWrapper() + .in(OnlCgreportItem::getCgrheadId, onlCgreportHeadIdList)); + List onlCgreportParamList = onlCgreportHeadIdList.isEmpty() + ? Collections.emptyList() + : iOnlCgreportParamService.list(new LambdaQueryWrapper() + .in(OnlCgreportParam::getCgrheadId, onlCgreportHeadIdList)); + if (onlCgreportHeadList.isEmpty()) { + continue; + } + for (Long tenantId : tenantIdSet) { + JSONObject msg = new JSONObject(); + msg.put("tenantId", tenantId); + msg.put("onlCgreportHeadList", onlCgreportHeadList); + msg.put("onlCgreportItemList", onlCgreportItemList); + msg.put("onlCgreportParamList", onlCgreportParamList); + dataSyncProducer.sendOrderly( + tenantId, + applicationConfig.getTenantSyncTopic(), + OnlCgreportHead.class.getSimpleName(), + TenantConstant.INSERT_TENANT_ONL_CGREPORT_HEAD_COMMAND, + msg.toJSONString(), + TenantConstant.MESSAGE_QUEUE_SELECTOR_KEY); + } + } + } + // 2. 表单元数据:按页面递归取整棵表树 + for (Long pageId : pageIds) { + String sql = getOnlFormHeadByPageIdSQL(pageId); + List headList = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(OnlFormHead.class), Collections.singleton(pageId)); + if (CollUtil.isEmpty(headList)) { + continue; + } + List findList = onlFormFindService.list(new LambdaQueryWrapper().in(OnlFormFind::getHeadId, headList)); + List fieldList = onlFormFieldService.list(new LambdaQueryWrapper().in(OnlFormField::getHeadId, headList)); + List checkList = onlFormCheckService.list(new LambdaQueryWrapper().in(OnlFormCheck::getHeadId, headList)); + List indexList = onlFormIndexService.list(new LambdaQueryWrapper().in(OnlFormIndex::getHeadId, headList)); + List keyList = onlFormKeyService.list(new LambdaQueryWrapper().in(OnlFormKey::getHeadId, headList)); + List pageList = onlFormPageService.list(new LambdaQueryWrapper().in(OnlFormPage::getHeadId, headList)); + for (Long tenantId : tenantIdSet) { + JSONObject msg = new JSONObject(); + msg.put("tenantId", tenantId); + msg.put("onlFormHeadList", headList); + msg.put("onlFormFindList", findList); + msg.put("onlFormFieldList", fieldList); + msg.put("onlFormCheckList", checkList); + msg.put("onlFormIndexList", indexList); + msg.put("onlFormKeyList", keyList); + msg.put("onlFormPageList", pageList); dataSyncProducer.sendOrderly( tenantId, applicationConfig.getTenantSyncTopic(), - OnlCgreportHead.class.getSimpleName(), - TenantConstant.INSERT_TENANT_ONL_CGREPORT_HEAD_COMMAND, - onlCgreportHeadMsgJsonData.toJSONString(), + OnlFormHead.class.getSimpleName(), + TenantConstant.INSERT_TENANT_ONL_FORM_HEAD_COMMAND, + msg.toJSONString(), TenantConstant.MESSAGE_QUEUE_SELECTOR_KEY); } } - //根据页面ID获取表单元数据列表 - String sql = getOnlFormHeadByPageIdSQL(pageId); - List headList = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(OnlFormHead.class), Collections.singleton(pageId)); - List findList = onlFormFindService.list(new LambdaQueryWrapper().in(OnlFormFind::getHeadId, headList)); - List fieldList = onlFormFieldService.list(new LambdaQueryWrapper().in(OnlFormField::getHeadId, headList)); - List checkList = onlFormCheckService.list(new LambdaQueryWrapper().in(OnlFormCheck::getHeadId, headList)); - List indexList = onlFormIndexService.list(new LambdaQueryWrapper().in(OnlFormIndex::getHeadId, headList)); - List keyList = onlFormKeyService.list(new LambdaQueryWrapper().in(OnlFormKey::getHeadId, headList)); - List pageList = onlFormPageService.list(new LambdaQueryWrapper().in(OnlFormPage::getHeadId, headList)); - // 表单元数据通知更新新增 - for(Long tenantId : tenantIdSet){ - JSONObject messageJsonData = new JSONObject(); - messageJsonData.put("tenantId", tenantId); - 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; } /** diff --git a/application-tenant/tenant-admin/src/main/java/apelet/tenantadmin/tenant/service/impl/SysTenantRoleServiceImpl.java b/application-tenant/tenant-admin/src/main/java/apelet/tenantadmin/tenant/service/impl/SysTenantRoleServiceImpl.java index 6bd244a..599a4a8 100644 --- a/application-tenant/tenant-admin/src/main/java/apelet/tenantadmin/tenant/service/impl/SysTenantRoleServiceImpl.java +++ b/application-tenant/tenant-admin/src/main/java/apelet/tenantadmin/tenant/service/impl/SysTenantRoleServiceImpl.java @@ -2,8 +2,6 @@ package apelet.tenantadmin.tenant.service.impl; import apelet.common.generator.model.*; import apelet.common.generator.service.*; -import apelet.common.online.model.OnlineForm; -import apelet.common.online.service.OnlineFormService; import apelet.tenantadmin.tenant.service.OnlineFormTenantSyncService; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; @@ -27,15 +25,11 @@ import apelet.tenantadmin.tenant.model.*; import apelet.tenantadmin.tenant.service.SysTenantRoleService; import apelet.tenantadmin.tenant.service.SysTenantService; import apelet.tenantadmin.config.DataSourceType; -import com.jayway.jsonpath.JsonPath; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.NotNull; 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; @@ -75,32 +69,7 @@ public class SysTenantRoleServiceImpl extends BaseService i private ApplicationConfig applicationConfig; @Autowired - private OnlineFormService onlineFormService; - - @Autowired - private IOnlCgreportHeadService iOnlCgreportHeadService; - - @Autowired - private IOnlCgreportItemService iOnlCgreportItemService; - - @Autowired - private IOnlCgreportParamService iOnlCgreportParamService; - - @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; + private OnlineFormTenantSyncService onlineFormTenantSyncService; private static final String TENANT_ID_KEY = "tenantId"; private static final String TENANT_ID_SET_KEY = "tenantIdSet"; @@ -527,86 +496,14 @@ public class SysTenantRoleServiceImpl extends BaseService i messageJsonData.toJSONString(), TenantConstant.MESSAGE_QUEUE_SELECTOR_KEY); - //表单元数据与查询数据源新增同步 - List onlineFormListByPageIds = onlineFormService.getOnlineFormListByPageIds((Set) pageIds); - for (OnlineForm onlineForm : onlineFormListByPageIds) { - // 根据表单json中的relativeFormCode获取到对应的查询数据源数据 - String widgetJson = onlineForm.getWidgetJson(); - List relativeFormCodeList = parseRelativeFormCodes(widgetJson); - if (!relativeFormCodeList.isEmpty()) { - List onlCgreportHeadList = iOnlCgreportHeadService.list(new LambdaQueryWrapper() - .in(OnlCgreportHead::getCode, relativeFormCodeList)); - List onlCgreportHeadIdList = onlCgreportHeadList.stream().map(OnlCgreportHead::getId).collect(Collectors.toList()); - List onlCgreportItemList = onlCgreportHeadIdList.isEmpty() - ? Collections.emptyList() - : iOnlCgreportItemService.list(new LambdaQueryWrapper() - .in(OnlCgreportItem::getCgrheadId, onlCgreportHeadIdList)); - List onlCgreportParamList = onlCgreportHeadIdList.isEmpty() - ? Collections.emptyList() - : iOnlCgreportParamService.list(new LambdaQueryWrapper() - .in(OnlCgreportParam::getCgrheadId, onlCgreportHeadIdList)); - for (Long tenantId : tenantIds) { - //表单数据源更新同步新增 - JSONObject onlCgreportHeadMsgJsonData = new JSONObject(); - onlCgreportHeadMsgJsonData.put("tenantId", tenantId); - 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(pageIds); - List headList = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(OnlFormHead.class), pageIds); - List findList = onlFormFindService.list(new LambdaQueryWrapper().in(OnlFormFind::getHeadId, headList)); - List fieldList = onlFormFieldService.list(new LambdaQueryWrapper().in(OnlFormField::getHeadId, headList)); - List checkList = onlFormCheckService.list(new LambdaQueryWrapper().in(OnlFormCheck::getHeadId, headList)); - List indexList = onlFormIndexService.list(new LambdaQueryWrapper().in(OnlFormIndex::getHeadId, headList)); - List keyList = onlFormKeyService.list(new LambdaQueryWrapper().in(OnlFormKey::getHeadId, headList)); - List pageList = onlFormPageService.list(new LambdaQueryWrapper().in(OnlFormPage::getHeadId, headList)); - // 表单元数据通知更新新增 - for(Long tenantId : tenantIds){ - JSONObject messageJsonData1 = new JSONObject(); - messageJsonData1.put("tenantId", tenantId); - messageJsonData1.put("onlFormHeadList", headList); - messageJsonData1.put("onlFormFindList", findList); - messageJsonData1.put("onlFormFieldList", fieldList); - messageJsonData1.put("onlFormCheckList", checkList); - messageJsonData1.put("onlFormIndexList", indexList); - messageJsonData1.put("onlFormKeyList", keyList); - messageJsonData1.put("onlFormPageList", pageList); - dataSyncProducer.sendOrderly( - tenantId, - applicationConfig.getTenantSyncTopic(), - OnlFormHead.class.getSimpleName(), - TenantConstant.INSERT_TENANT_ONL_FORM_HEAD_COMMAND, - messageJsonData1.toJSONString(), - TenantConstant.MESSAGE_QUEUE_SELECTOR_KEY - ); - } + // 只同步表单元数据与查询数据源;widgetJson 由「表单升级同步」单独推。 + // 只推给本次映射有变化的租户 + List changedTenantIds = resultTenantExtList.stream() + .map(SysTenantExt::getTenantId).collect(Collectors.toList()); + onlineFormTenantSyncService.syncFormMetadata(pageIds, changedTenantIds); } } - private static @NotNull String getOnlFormHeadByPageIdSQL(Collection pageIdList) { - String placeholders = String.join(",", Collections.nCopies(pageIdList.size(), "?")); - 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; - } @@ -752,25 +649,6 @@ public class SysTenantRoleServiceImpl extends BaseService i return resultMenuIdSet; } - /** - * 从 widgetJson 中提取所有的 relativeFormCode(查询数据源编码)。 - * widgetJson 为空、非法 JSON 或没有该节点时返回空集合,不抛异常。 - */ - private List parseRelativeFormCodes(String widgetJson) { - if (StrUtil.isBlank(widgetJson)) { - return Collections.emptyList(); - } - try { - List codes = JsonPath.read(widgetJson, "$..relativeFormCode"); - if (codes == null) { - return Collections.emptyList(); - } - return codes.stream().filter(StrUtil::isNotBlank).distinct().collect(Collectors.toList()); - } catch (Exception e) { - log.warn("解析 widgetJson 的 relativeFormCode 失败,按无查询数据源处理:{}", e.getMessage()); - return Collections.emptyList(); - } - } @Data @NoArgsConstructor diff --git a/application-tenant/tenant-sync/pom.xml b/application-tenant/tenant-sync/pom.xml index 76d3dd4..b547db6 100644 --- a/application-tenant/tenant-sync/pom.xml +++ b/application-tenant/tenant-sync/pom.xml @@ -38,7 +38,7 @@ apelet common-online - 1.0.6 + 1.0.7 diff --git a/application-tenant/tenant-sync/src/main/java/apelet/tenantsync/aop/DynamicTenantDatasourceAspect.java b/application-tenant/tenant-sync/src/main/java/apelet/tenantsync/aop/DynamicTenantDatasourceAspect.java index 673c839..1cead8a 100644 --- a/application-tenant/tenant-sync/src/main/java/apelet/tenantsync/aop/DynamicTenantDatasourceAspect.java +++ b/application-tenant/tenant-sync/src/main/java/apelet/tenantsync/aop/DynamicTenantDatasourceAspect.java @@ -1,8 +1,12 @@ package apelet.tenantsync.aop; import apelet.common.core.config.DataSourceContextHolder; +import apelet.common.datasync.base.service.BaseDataSyncConsumerService; import apelet.common.datasync.service.DataSyncConsumerTransService; +import apelet.common.tenant.service.SyncLogService; import apelet.tenantsync.annotation.SwitchTenantDatasource; +import apelet.tenantsync.config.DataSourceType; +import com.alibaba.fastjson.JSONObject; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; @@ -38,6 +42,8 @@ public class DynamicTenantDatasourceAspect { private TransactionDefinition transactionDefinition; @Autowired private DataSyncConsumerTransService consumerTransService; + @Autowired + private SyncLogService syncLogService; @Pointcut("@annotation(apelet.tenantsync.annotation.SwitchTenantDatasource)") public void tenantDatasourcePointCut() { @@ -48,19 +54,36 @@ public class DynamicTenantDatasourceAspect { public Object around(ProceedingJoinPoint point) throws Throwable { SwitchTenantDatasource ds = this.getAnnotation(point); Integer datasourceType = ds.datasourceType(); + Object[] args = point.getArgs(); String transId; if (datasourceType == -1) { - datasourceType = (Integer) point.getArgs()[0]; - transId = point.getArgs()[1].toString(); + datasourceType = (Integer) args[0]; + transId = args[1].toString(); } else { - transId = point.getArgs()[0].toString(); + transId = args[0].toString(); } - transId += this.getMethod(point).getName(); + // 注意:transId 已包含租户Id/类型/命令/时间戳,不再拼接方法名, + // 否则与消息里的流水号对不上,也不便于和日志关联。 + // doHandle 签名为 (transId, messageCommand, messageJsonData): + // datasourceType == -1 时 args[0]=datasourceType, args[1]=transId, args[2]=command, args[3]=json + // 否则 args[0]=transId, args[1]=command, args[2]=json + int base = datasourceType == -1 ? 1 : 0; + String messageCommand = args.length > base + 1 && args[base + 1] != null ? args[base + 1].toString() : null; + Object rawJson = args.length > base + 2 ? args[base + 2] : null; + JSONObject messageJson = rawJson instanceof JSONObject ? (JSONObject) rawJson : null; + Long tenantId = messageJson == null ? null : messageJson.getLong("tenantId"); + String contentSummary = messageJson == null ? null : messageJson.toJSONString(); + long start = System.currentTimeMillis(); + // 通过判断 DataSource 中的值来判断当前方法应用哪个数据源 Integer originalType = DataSourceContextHolder.setDataSourceType(datasourceType); log.debug("set datasource is [{}].", datasourceType); + String messageType = resolveMessageType(point); if (consumerTransService.exist(transId)) { log.info("TransId {} has been processed before.", transId); + recordLog(transId, tenantId, messageType, messageCommand, "SKIPPED", + "重复消息,已跳过", contentSummary, System.currentTimeMillis() - start); + DataSourceContextHolder.unset(originalType); return null; } TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition); @@ -68,10 +91,15 @@ public class DynamicTenantDatasourceAspect { Object r = point.proceed(); consumerTransService.saveNew(transId); transactionManager.commit(transactionStatus); + recordLog(transId, tenantId, messageType, messageCommand, "SUCCESS", + null, contentSummary, System.currentTimeMillis() - start); return r; } catch (Exception e) { log.error(e.getMessage(), e); transactionManager.rollback(transactionStatus); + recordLog(transId, tenantId, messageType, messageCommand, "FAILED", + e.getClass().getSimpleName() + ": " + e.getMessage(), contentSummary, + System.currentTimeMillis() - start); throw e; } finally { DataSourceContextHolder.unset(originalType); @@ -79,6 +107,39 @@ public class DynamicTenantDatasourceAspect { } } + /** + * 写同步日志。日志本身落到管理端库,与业务事务解耦,失败不影响主流程。 + */ + private void recordLog(String transId, Long tenantId, String messageType, String messageCommand, + String status, String errorMsg, String contentSummary, long costMs) { + try { + Integer originalType = DataSourceContextHolder.setDataSourceType(DataSourceType.TENANT_ADMIN); + try { + syncLogService.record(transId, tenantId, messageType, messageCommand, + status, errorMsg, contentSummary, costMs); + } finally { + DataSourceContextHolder.unset(originalType); + } + } catch (Exception e) { + log.warn("写同步日志失败,transId={}: {}", transId, e.getMessage()); + } + } + + /** + * 取消费者声明的消息类型(如 OnlFormHead);解析失败返回 null,不影响主流程。 + */ + private String resolveMessageType(ProceedingJoinPoint point) { + try { + Object target = point.getTarget(); + if (target instanceof BaseDataSyncConsumerService) { + return ((BaseDataSyncConsumerService) target).handleMessageType(); + } + } catch (Exception e) { + log.debug("resolveMessageType failed: {}", e.getMessage()); + } + return null; + } + private Method getMethod(JoinPoint joinPoint) { Signature signature = joinPoint.getSignature(); MethodSignature methodSignature = (MethodSignature) signature; diff --git a/application-tenant/tenant-sync/src/main/java/apelet/tenantsync/online/service/SysTenantOnlCgreporHeadServiceImpl.java b/application-tenant/tenant-sync/src/main/java/apelet/tenantsync/online/service/SysTenantOnlCgreporHeadServiceImpl.java index 3259b6b..2e8014c 100644 --- a/application-tenant/tenant-sync/src/main/java/apelet/tenantsync/online/service/SysTenantOnlCgreporHeadServiceImpl.java +++ b/application-tenant/tenant-sync/src/main/java/apelet/tenantsync/online/service/SysTenantOnlCgreporHeadServiceImpl.java @@ -7,6 +7,8 @@ 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.annotation.SwitchTenantDatasource; +import apelet.tenantsync.config.DataSourceType; import apelet.tenantsync.consumer.TenantSyncConsumer; import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson.JSONObject; @@ -31,6 +33,7 @@ public class SysTenantOnlCgreporHeadServiceImpl implements BaseDataSyncConsumerS @Autowired private IOnlCgreportParamService iOnlCgreportParamService; + @SwitchTenantDatasource(datasourceType = DataSourceType.TENANT_COMMON) @Override public void doHandle(String transId, String messageCommand, JSONObject messageJsonData) { if (StrUtil.equals(messageCommand, TenantConstant.INSERT_TENANT_ONL_CGREPORT_HEAD_COMMAND)) { diff --git a/application-tenant/tenant-sync/src/main/java/apelet/tenantsync/online/service/SysTenantOnlFormHeadServiceImpl.java b/application-tenant/tenant-sync/src/main/java/apelet/tenantsync/online/service/SysTenantOnlFormHeadServiceImpl.java index 0dedfa9..f8b83bd 100644 --- a/application-tenant/tenant-sync/src/main/java/apelet/tenantsync/online/service/SysTenantOnlFormHeadServiceImpl.java +++ b/application-tenant/tenant-sync/src/main/java/apelet/tenantsync/online/service/SysTenantOnlFormHeadServiceImpl.java @@ -5,6 +5,8 @@ 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.annotation.SwitchTenantDatasource; +import apelet.tenantsync.config.DataSourceType; import apelet.tenantsync.consumer.TenantSyncConsumer; import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson.JSONObject; @@ -12,9 +14,9 @@ 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.Set; import java.util.stream.Collectors; @Slf4j @@ -36,6 +38,7 @@ public class SysTenantOnlFormHeadServiceImpl implements BaseDataSyncConsumerServ @Autowired private IOnlFormPageService onlFormPageService; + @SwitchTenantDatasource(datasourceType = DataSourceType.TENANT_COMMON) @Override public void doHandle(String transId, String messageCommand, JSONObject messageJsonData) { if (StrUtil.equals(messageCommand, TenantConstant.INSERT_TENANT_ONL_FORM_HEAD_COMMAND)) { @@ -57,9 +60,24 @@ public class SysTenantOnlFormHeadServiceImpl implements BaseDataSyncConsumerServ return; } //对需要同步的表单元数据进行多租户处理 + // 先收集原始表名:sort 里存的是主表名,必须判断它是否在本次同步范围内,再决定是否加后缀 + Set originTableNames = onlFormHeadList.stream() + .map(OnlFormHead::getTableName) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toSet()); + String suffix = "_" + tenantId; for (OnlFormHead onlFormHead : onlFormHeadList) { + String sort = onlFormHead.getSort(); + if (StrUtil.isNotBlank(sort)) { + if (originTableNames.contains(sort)) { + onlFormHead.setSort(sort + suffix); + } else { + log.warn("分录 sort 指向的表不在本次同步范围内,保持原样。tenantId={}, tableName={}, sort={}", + tenantId, onlFormHead.getTableName(), sort); + } + } onlFormHead.setTenantId(tenantId); - onlFormHead.setTableName(onlFormHead.getTableName() + "_" + tenantId); + onlFormHead.setTableName(onlFormHead.getTableName() + suffix); } for (OnlFormHead onlFormHead : onlFormHeadList) { //判断对应租户是否已经存在相同名称的表单元数据 diff --git a/application-tenant/tenant-sync/src/main/java/apelet/tenantsync/online/service/SysTenantOnlineFormUpdateServiceImpl.java b/application-tenant/tenant-sync/src/main/java/apelet/tenantsync/online/service/SysTenantOnlineFormUpdateServiceImpl.java index c90c482..53fef12 100644 --- a/application-tenant/tenant-sync/src/main/java/apelet/tenantsync/online/service/SysTenantOnlineFormUpdateServiceImpl.java +++ b/application-tenant/tenant-sync/src/main/java/apelet/tenantsync/online/service/SysTenantOnlineFormUpdateServiceImpl.java @@ -6,20 +6,27 @@ 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.online.util.OnlineRedisKeyUtil; import apelet.common.sequence.wrapper.IdGeneratorWrapper; import apelet.common.tenant.constant.TenantConstant; +import apelet.tenantsync.annotation.SwitchTenantDatasource; +import apelet.tenantsync.config.DataSourceType; 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.JSONArray; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; @Slf4j @Service @@ -31,43 +38,122 @@ public class SysTenantOnlineFormUpdateServiceImpl implements BaseDataSyncConsume private OnlineFormMapper onlineFormMapper; @Autowired private OnlineFormDatasourceMapper onlineFormDatasourceMapper; + @Autowired + private RedissonClient redissonClient; + @SwitchTenantDatasource(datasourceType = DataSourceType.TENANT_COMMON) @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 datasourceIdSet = messageJsonData.getJSONArray("datasourceIdSet").toJavaList(Long.class); - if (originalOnlineForm == null) { - originalOnlineForm = onlineForm; + Long tenantId = messageJsonData.getLong("tenantId"); + if (tenantId == null) { + log.warn("在线表单同步缺少 tenantId,已跳过。transId={}", transId); + return; + } + JSONObject adminFormJson = messageJsonData.getJSONObject("adminOnlineForm"); + if (adminFormJson == null) { + log.warn("在线表单同步缺少 adminOnlineForm,已跳过。transId={}, tenantId={}", transId, tenantId); + return; } - //获取租户数据库中所有对应form_code的表单数据 - List dbOnlineFormList = onlineFormMapper.selectList(new QueryWrapper().eq("form_code", onlineForm.getFormCode())); + OnlineForm onlineForm = adminFormJson.toJavaObject(OnlineForm.class); + JSONArray datasourceIdArray = messageJsonData.getJSONArray("datasourceIdSet"); + List datasourceIds = datasourceIdArray == null ? Collections.emptyList() : datasourceIdArray.toJavaList(Long.class); + + // 只处理本租户的表单,避免共享库下跨租户改写 + List dbOnlineFormList = onlineFormMapper.selectList(new QueryWrapper() + .eq("form_code", onlineForm.getFormCode()) + .eq("tenant_id", tenantId)); 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数据 + // 只追加:A 端已有的控件/字段一律不动,仅补上 B 端新加的 + JSONObject tenantJson = JSON.parseObject(dbOnlineForm.getWidgetJson()); + JSONObject baseJson = JSON.parseObject(onlineForm.getWidgetJson()); + if (tenantJson == null || baseJson == null) { + log.warn("表单 widgetJson 为空,跳过合并。formId={}, tenantId={}", dbOnlineForm.getFormId(), tenantId); + continue; + } + appendMissing(tenantJson, baseJson); + dbOnlineForm.setWidgetJson(tenantJson.toJSONString()); onlineFormMapper.update(dbOnlineForm, new QueryWrapper().eq("form_id", dbOnlineForm.getFormId())); + // 同步绕过 service,需手动清缓存,否则 A 端仍渲染旧表单 + redissonClient.getBucket(OnlineRedisKeyUtil.makeOnlineFormKey(dbOnlineForm.getFormId())).delete(); + redissonClient.getBucket(OnlineRedisKeyUtil.makeOnlineFormDatasourceKey(dbOnlineForm.getFormId())).delete(); } - //按照同步过来的datasourceIdSet更新数据 - OnlineFormDatasource formDatasourceFilter = new OnlineFormDatasource(); - formDatasourceFilter.setFormId(onlineForm.getFormId()); - onlineFormDatasourceMapper.delete(new QueryWrapper<>(formDatasourceFilter)); - if (CollUtil.isNotEmpty(datasourceIdSet)) { - for (Long datasourceId : datasourceIdSet) { + + // 按同步过来的 datasourceIdSet 重建关联(同样限定本租户的表单) + for (OnlineForm dbOnlineForm : dbOnlineFormList) { + onlineFormDatasourceMapper.delete(new QueryWrapper() + .eq("form_id", dbOnlineForm.getFormId())); + for (Long datasourceId : datasourceIds) { OnlineFormDatasource onlineFormDatasource = new OnlineFormDatasource(); onlineFormDatasource.setId(idGenerator.nextLongId()); - onlineFormDatasource.setFormId(onlineForm.getFormId()); + onlineFormDatasource.setFormId(dbOnlineForm.getFormId()); onlineFormDatasource.setDatasourceId(datasourceId); onlineFormDatasourceMapper.insert(onlineFormDatasource); } } + } + } + /** + * 把 base 里有、tenant 里没有的节点追加进 tenant,已有节点一律不覆盖(A 端个性化优先)。 + * 数组按 variableName 识别元素身份,未带 variableName 的元素视为不参与合并、直接追加。 + */ + private void appendMissing(JSONObject tenant, JSONObject base) { + for (Map.Entry entry : base.entrySet()) { + String key = entry.getKey(); + Object baseValue = entry.getValue(); + if (!tenant.containsKey(key)) { + tenant.put(key, baseValue); + continue; + } + Object tenantValue = tenant.get(key); + if (tenantValue instanceof JSONObject && baseValue instanceof JSONObject) { + appendMissing((JSONObject) tenantValue, (JSONObject) baseValue); + } else if (tenantValue instanceof JSONArray && baseValue instanceof JSONArray) { + appendMissingArray((JSONArray) tenantValue, (JSONArray) baseValue); + } + // 标量或类型不一致:保留 A 端的值 + } + } + + private void appendMissingArray(JSONArray tenantArray, JSONArray baseArray) { + Set existing = tenantArray.stream() + .filter(JSONObject.class::isInstance) + .map(o -> identityKey((JSONObject) o)) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toSet()); + for (Object baseElement : baseArray) { + if (!(baseElement instanceof JSONObject)) { + tenantArray.add(baseElement); + continue; + } + JSONObject baseObject = (JSONObject) baseElement; + String key = identityKey(baseObject); + if (StrUtil.isBlank(key)) { + // 无任何身份标识,只能整元素比对去重,避免重复同步时越堆越多 + if (!tenantArray.contains(baseObject)) { + tenantArray.add(baseObject); + } + continue; + } + if (existing.add(key)) { + tenantArray.add(baseObject); + } + } + } + + /** + * 数组元素的身份键。控件用 variableName;按钮/事件用 id;插件用 pluginType; + * 表头列用 columnId/showFieldName;都没有则返回 null(退化为整元素比对)。 + */ + private String identityKey(JSONObject element) { + for (String field : new String[]{"variableName", "id", "pluginType", "columnId", "showFieldName"}) { + String value = element.getString(field); + if (StrUtil.isNotBlank(value)) { + return field + ":" + value; + } } + return null; } @Override diff --git a/application/upms/upms-service/pom.xml b/application/upms/upms-service/pom.xml index 0a90a62..f2795e8 100644 --- a/application/upms/upms-service/pom.xml +++ b/application/upms/upms-service/pom.xml @@ -53,7 +53,7 @@ apelet common-online - 1.0.6 + 1.0.7 apelet diff --git a/common/common-flow-online/pom.xml b/common/common-flow-online/pom.xml index 9d36a1c..79a1bc4 100644 --- a/common/common-flow-online/pom.xml +++ b/common/common-flow-online/pom.xml @@ -23,7 +23,7 @@ apelet common-online - 1.0.6 + 1.0.7 apelet diff --git a/common/common-generator/src/main/java/apelet/common/generator/controller/OnlFormHeadController.java b/common/common-generator/src/main/java/apelet/common/generator/controller/OnlFormHeadController.java index d3b7dbc..782cc84 100644 --- a/common/common-generator/src/main/java/apelet/common/generator/controller/OnlFormHeadController.java +++ b/common/common-generator/src/main/java/apelet/common/generator/controller/OnlFormHeadController.java @@ -123,6 +123,14 @@ public class OnlFormHeadController { @PostMapping(value = "/addAll") @Schema(description = "添加主数据和分录数据") public ResponseResult addAll(@MyRequestBody OnlFormHeadDto dto) { + // 管理端(B端)不允许占用租户字段前缀 tenant_,否则租户端无法区分自定义字段 + if (isAdminRequest() + && dto != null + && dto.getOnlFormFieldList() != null + && dto.getOnlFormFieldList().stream().anyMatch(f -> f.getFieldName() != null + && f.getFieldName().toLowerCase().startsWith("tenant_"))) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED); + } if (ObjectUtil.isNotEmpty(dto)) { ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED); } @@ -145,6 +153,14 @@ public class OnlFormHeadController { @PostMapping(value = "/editAll") @Schema(description = "编辑主数据和分录数据") public ResponseResult editAll(@MyRequestBody OnlFormHeadDto dto) { + // 管理端(B端)不允许占用租户字段前缀 tenant_,否则租户端无法区分自定义字段 + if (isAdminRequest() + && dto != null + && dto.getOnlFormFieldList() != null + && dto.getOnlFormFieldList().stream().anyMatch(f -> f.getFieldName() != null + && f.getFieldName().toLowerCase().startsWith("tenant_"))) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED); + } if (ObjectUtil.isNotEmpty(dto)) { ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED); } @@ -439,4 +455,12 @@ public class OnlFormHeadController { } + /** + * 当前是否管理端(B端)请求:无 TokenData(非 HTTP 场景)或 TokenData 无 tenantId 都按管理端处理。 + */ + private boolean isAdminRequest() { + apelet.common.core.object.TokenData tokenData = apelet.common.core.object.TokenData.takeFromRequest(); + return tokenData == null || tokenData.getTenantId() == null; + } + } diff --git a/common/common-online/src/main/java/apelet/common/online/controller/OnlineFormController.java b/common/common-online/src/main/java/apelet/common/online/controller/OnlineFormController.java index 226032f..3ea6e04 100644 --- a/common/common-online/src/main/java/apelet/common/online/controller/OnlineFormController.java +++ b/common/common-online/src/main/java/apelet/common/online/controller/OnlineFormController.java @@ -35,6 +35,7 @@ import apelet.common.online.model.constant.FormType; import apelet.common.online.service.*; import apelet.common.online.util.OnlineRedisKeyUtil; import apelet.common.online.util.QuickFormWidgetUtil; +import apelet.common.online.util.TenantFieldUtil; import apelet.common.online.util.WidgetFieldTypeMapping; import apelet.common.online.util.WidgetJsonUtil; import apelet.common.online.vo.OnlineFormVo; @@ -1150,6 +1151,19 @@ public class OnlineFormController { List existingFields = onlFormFieldService.list(new LambdaQueryWrapper().eq(OnlFormField::getHeadId, head.getId())); Map fieldByColumnName = existingFields.stream().collect(Collectors.toMap(OnlFormField::getFieldName, Function.identity(), (a, b) -> a)); + // 租户端新增的字段统一加 tenant_ 前缀,与 B 端下发的标准字段区分(已存在的字段不动) + if (TenantFieldUtil.isTenantRequest()) { + for (String rawColumnName : new ArrayList<>(columnWidgetMap.keySet())) { + if (fieldByColumnName.containsKey(rawColumnName)) continue; + String prefixed = TenantFieldUtil.applyTenantPrefix(rawColumnName); + if (prefixed.equals(rawColumnName)) continue; + // widget 绑定列名同步改写,只改归属本表的控件(避免主表/子表同名误改) + TenantFieldUtil.renameColumnNames(widgetJson, table.getTableId(), table.getTableName(), + name -> rawColumnName.equals(name) ? prefixed : name); + JSONObject widget = columnWidgetMap.remove(rawColumnName); + if (widget != null) columnWidgetMap.put(prefixed, widget); + } + } List newColumnNames = new ArrayList<>(); for (Map.Entry columnEntry : columnWidgetMap.entrySet()) { String columnName = columnEntry.getKey(); diff --git a/common/common-online/src/main/java/apelet/common/online/util/TenantFieldUtil.java b/common/common-online/src/main/java/apelet/common/online/util/TenantFieldUtil.java new file mode 100644 index 0000000..9403c6f --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/util/TenantFieldUtil.java @@ -0,0 +1,150 @@ +package apelet.common.online.util; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import org.apache.commons.lang3.StringUtils; + +/** + * 租户自定义字段的命名约定。 + *

+ * A 端(租户端)新增的字段统一加 {@code tenant_} 前缀,与 B 端(管理端)下发的标准字段区分开; + * B 端新增字段时禁止使用该前缀(见 OnlineFormController 的校验)。 + *

+ * 字段名在各处统一小写(MetaCacheService、syncNewFieldsAndSubTables 均有 toLowerCase), + * 因此前缀判断前先归一化。 + * + * @author chenchuchuan + */ +public final class TenantFieldUtil { + + public static final String TENANT_FIELD_PREFIX = "tenant_"; + + /** + * 当前请求是否来自租户端(A端)。 + *

+ * 无 TokenData(定时任务、内部调用等非 HTTP 场景)按管理端处理。 + * + * @return true 表示租户端请求。 + */ + public static boolean isTenantRequest() { + apelet.common.core.object.TokenData tokenData = apelet.common.core.object.TokenData.takeFromRequest(); + return tokenData != null && tokenData.getTenantId() != null; + } + + /** + * 判断是否租户自定义字段(按列名)。 + * + * @param fieldName 数据库列名,如 tenant_budget。 + * @return true 表示租户端新增的字段。 + */ + public static boolean isTenantField(String fieldName) { + return StringUtils.isNotBlank(fieldName) + && fieldName.toLowerCase().startsWith(TENANT_FIELD_PREFIX); + } + + /** + * 给租户自定义字段加前缀;已带前缀则原样返回。 + * + * @param fieldName 原始列名,如 budget。 + * @return 带前缀的列名,如 tenant_budget。 + */ + public static String applyTenantPrefix(String fieldName) { + if (StringUtils.isBlank(fieldName) || isTenantField(fieldName)) { + return fieldName; + } + return TENANT_FIELD_PREFIX + fieldName; + } + + /** + * 把 widgetJson 里指定表下所有字段控件的绑定列名改写为 {@code fn} 的返回值。 + *

+ * 只改归属 {@code table} 的控件,避免主表/子表同名时误改。 + * 取值口径与 {@link WidgetJsonUtil#buildTableFieldMap(JSONObject)} 一致: + * 优先 bindData.columnName,兼容仅有 columnFieldName 的移动端字段; + * 表头控件(widgetType=100)读取 props.tableColumnList。 + * + * @param widgetJson pc/mobile 根节点。 + * @param tableId 目标表Id,可为 null。 + * @param tableName 目标表名,可为 null。 + * @param fn 列名转换函数,入参/出参均为列名。 + */ + public static void renameColumnNames(JSONObject widgetJson, Long tableId, String tableName, + java.util.function.UnaryOperator fn) { + if (widgetJson == null || fn == null) return; + for (String mode : new String[]{"pc", "mobile"}) { + JSONObject modeObj = widgetJson.getJSONObject(mode); + if (modeObj == null) continue; + renameInWidgets(modeObj.getJSONArray("widgetList"), tableId, tableName, fn); + JSONObject tableWidget = modeObj.getJSONObject("tableWidget"); + if (tableWidget != null) { + renameInWidgets(tableWidget.getJSONArray("childWidgetList"), tableId, tableName, fn); + } + } + } + + private static void renameInWidgets(JSONArray widgetList, Long tableId, String tableName, + java.util.function.UnaryOperator fn) { + if (widgetList == null || widgetList.isEmpty()) return; + for (int i = 0; i < widgetList.size(); i++) { + JSONObject widget = widgetList.getJSONObject(i); + if (widget == null) continue; + Integer widgetType = widget.getInteger("widgetType"); + if (widgetType != null && widgetType == 100) { + renameInTableColumns(widget, tableId, tableName, fn); + continue; + } + JSONArray childWidgetList = widget.getJSONArray("childWidgetList"); + if (childWidgetList != null && !childWidgetList.isEmpty()) { + renameInWidgets(childWidgetList, tableId, tableName, fn); + continue; + } + JSONObject bindData = widget.getJSONObject("bindData"); + if (bindData == null || !tableMatched(bindData, tableId, tableName)) continue; + String columnName = bindData.getString("columnName"); + if (StringUtils.isNotEmpty(columnName)) { + bindData.put("columnName", fn.apply(columnName)); + } + String columnFieldName = bindData.getString("columnFieldName"); + if (StringUtils.isNotEmpty(columnFieldName)) { + bindData.put("columnFieldName", fn.apply(columnFieldName)); + } + } + } + + private static void renameInTableColumns(JSONObject tableWidget, Long tableId, String tableName, + java.util.function.UnaryOperator fn) { + JSONObject bindData = tableWidget.getJSONObject("bindData"); + JSONObject props = tableWidget.getJSONObject("props"); + if (bindData == null || props == null || !tableMatched(bindData, tableId, tableName)) return; + JSONArray tableColumnList = props.getJSONArray("tableColumnList"); + if (tableColumnList == null || tableColumnList.isEmpty()) return; + for (int i = 0; i < tableColumnList.size(); i++) { + JSONObject tableColumn = tableColumnList.getJSONObject(i); + String showFieldName = tableColumn.getString("showFieldName"); + if (StringUtils.isNotEmpty(showFieldName)) { + tableColumn.put("showFieldName", fn.apply(showFieldName)); + } + // 新增列:字段名临时存在 columnId 里(非数字),见 WidgetJsonUtil.backfillTableColumns 的约定 + String colIdStr = tableColumn.getString("columnId"); + if (StringUtils.isNotEmpty(colIdStr) && !colIdStr.matches("\\d+")) { + tableColumn.put("columnId", fn.apply(colIdStr)); + } + } + } + + /** + * 与 {@link WidgetJsonUtil} 的归属判断保持一致:优先 tableId,其次忽略大小写的 tableName。 + */ + private static boolean tableMatched(JSONObject bindData, Long tableId, String tableName) { + String tableKey = bindData.getString("tableId"); + if (StringUtils.isEmpty(tableKey)) { + tableKey = bindData.getString("tableName"); + } + if (StringUtils.isEmpty(tableKey)) return false; + if (tableId != null && tableKey.equals(String.valueOf(tableId))) return true; + return StringUtils.isNotEmpty(tableName) && tableKey.equalsIgnoreCase(tableName); + } + + private TenantFieldUtil() { + } +} diff --git a/common/common-openapi/pom.xml b/common/common-openapi/pom.xml index c02b425..f2ece26 100644 --- a/common/common-openapi/pom.xml +++ b/common/common-openapi/pom.xml @@ -31,13 +31,13 @@ apelet common-online - 1.0.6 + 1.0.7 apelet common-generator - 1.0.5 + 1.0.6 diff --git a/common/common-tenant-online/pom.xml b/common/common-tenant-online/pom.xml index 48ed634..eecc03e 100644 --- a/common/common-tenant-online/pom.xml +++ b/common/common-tenant-online/pom.xml @@ -23,7 +23,7 @@ apelet common-online - 1.0.6 + 1.0.7 \ No newline at end of file diff --git a/common/common-tenant/src/main/java/apelet/common/tenant/dao/SyncLogMapper.java b/common/common-tenant/src/main/java/apelet/common/tenant/dao/SyncLogMapper.java new file mode 100644 index 0000000..fceac72 --- /dev/null +++ b/common/common-tenant/src/main/java/apelet/common/tenant/dao/SyncLogMapper.java @@ -0,0 +1,12 @@ +package apelet.common.tenant.dao; + +import apelet.common.core.base.dao.BaseDaoMapper; +import apelet.common.tenant.model.SyncLog; + +/** + * 租户同步日志访问接口。 + * + * @author chenchuchuan + */ +public interface SyncLogMapper extends BaseDaoMapper { +} diff --git a/common/common-tenant/src/main/java/apelet/common/tenant/model/SyncLog.java b/common/common-tenant/src/main/java/apelet/common/tenant/model/SyncLog.java new file mode 100644 index 0000000..9e8bf98 --- /dev/null +++ b/common/common-tenant/src/main/java/apelet/common/tenant/model/SyncLog.java @@ -0,0 +1,55 @@ +package apelet.common.tenant.model; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.util.Date; + +/** + * 租户同步日志。每条同步消息在租户端执行一条记录。 + * + * @author chenchuchuan + */ +@Data +@TableName(value = "zz_sync_log") +public class SyncLog { + + @TableId(value = "id", type = IdType.INPUT) + private Long id; + + /** + * 消息流水号,格式:{租户Id}-{雪花Id}-{消息类型}-{命令}-{时间戳}。 + */ + @TableField(value = "trans_id") + private String transId; + + @TableField(value = "tenant_id") + private Long tenantId; + + @TableField(value = "message_type") + private String messageType; + + @TableField(value = "message_command") + private String messageCommand; + + /** + * SUCCESS / FAILED / SKIPPED。 + */ + @TableField(value = "status") + private String status; + + @TableField(value = "error_msg") + private String errorMsg; + + @TableField(value = "content_summary") + private String contentSummary; + + @TableField(value = "cost_ms") + private Long costMs; + + @TableField(value = "create_time") + private Date createTime; +} diff --git a/common/common-tenant/src/main/java/apelet/common/tenant/service/SyncLogService.java b/common/common-tenant/src/main/java/apelet/common/tenant/service/SyncLogService.java new file mode 100644 index 0000000..a28c0cd --- /dev/null +++ b/common/common-tenant/src/main/java/apelet/common/tenant/service/SyncLogService.java @@ -0,0 +1,79 @@ +package apelet.common.tenant.service; + +import apelet.common.tenant.dao.SyncLogMapper; +import apelet.common.tenant.model.SyncLog; +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.Date; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +/** + * 租户同步日志服务。 + *

+ * 由 tenant-sync 的切面在每条消息执行结束后写入,记录哪个租户、什么类型、成功失败与原因。 + * 写日志失败不能影响主同步流程,因此本类内部吞掉异常只打 warn。 + * + * @author chenchuchuan + */ +@Slf4j +@Service +public class SyncLogService { + + /** + * 日志表主键。日志本身是辅助数据,不必走分布式 Id 生成器(common-tenant 也不依赖 common-sequence)。 + */ + private static final AtomicLong ID_SEQ = new AtomicLong(System.currentTimeMillis()); + + @Autowired + private SyncLogMapper syncLogMapper; + + /** + * 记录一条同步日志。 + * + * @param transId 消息流水号 + * @param tenantId 目标租户Id + * @param messageType 消息类型 + * @param messageCommand 消息命令 + * @param status SUCCESS / FAILED / SKIPPED + * @param errorMsg 失败原因 + * @param contentSummary 消息体摘要 + * @param costMs 耗时(毫秒) + */ + public void record(String transId, Long tenantId, String messageType, String messageCommand, + String status, String errorMsg, String contentSummary, Long costMs) { + try { + SyncLog syncLog = new SyncLog(); + syncLog.setId(ID_SEQ.incrementAndGet()); + syncLog.setTransId(transId); + syncLog.setTenantId(tenantId); + syncLog.setMessageType(messageType); + syncLog.setMessageCommand(messageCommand); + syncLog.setStatus(status); + syncLog.setErrorMsg(truncate(errorMsg, 2000)); + syncLog.setContentSummary(truncate(contentSummary, 2000)); + syncLog.setCostMs(costMs); + syncLog.setCreateTime(new Date()); + syncLogMapper.insert(syncLog); + } catch (Exception e) { + log.warn("写入同步日志失败,transId={}: {}", transId, e.getMessage()); + } + } + + /** + * 按租户 + 时间倒序查询最近的同步记录。 + */ + public List listRecent(Long tenantId, int limit) { + return syncLogMapper.selectList(new LambdaQueryWrapper() + .eq(tenantId != null, SyncLog::getTenantId, tenantId) + .orderByDesc(SyncLog::getCreateTime) + .last("LIMIT " + Math.max(1, Math.min(limit, 500)))); + } + + private String truncate(String value, int max) { + return value == null || value.length() <= max ? value : value.substring(0, max); + } +}