Browse Source

feat(tenant): 实现租户自定义字段tenant_前缀隔离机制

- 新增 TenantFieldUtil 工具类,定义 tenant_ 前缀命名约定
- 管理端新增/编辑字段时校验禁止占用 tenant_ 前缀
- 租户端新增字段自动追加 tenant_ 前缀并同步改写 widgetJson 列名
- 拆分 OnlineFormTenantSyncService 为 syncFormUpdate 和 syncFormMetadata
- SysTenantRoleServiceImpl 移除内联同步逻辑,委托统一服务调用
- 新增 SyncLog 模型、Mapper 及 SyncLogService 记录同步操作日志
- 升级 common-online 至 1.0.7、common-generator 至 1.0.6
feature/2026-10/tenant-multitenant
chenchuchuan 1 day ago
parent
commit
8bf1d4638c
  1. 2
      application-tenant/tenant-admin/pom.xml
  2. 11
      application-tenant/tenant-admin/src/main/java/apelet/tenantadmin/tenant/service/OnlineFormTenantSyncService.java
  3. 147
      application-tenant/tenant-admin/src/main/java/apelet/tenantadmin/tenant/service/impl/OnlineFormTenantSyncServiceImpl.java
  4. 134
      application-tenant/tenant-admin/src/main/java/apelet/tenantadmin/tenant/service/impl/SysTenantRoleServiceImpl.java
  5. 2
      application-tenant/tenant-sync/pom.xml
  6. 69
      application-tenant/tenant-sync/src/main/java/apelet/tenantsync/aop/DynamicTenantDatasourceAspect.java
  7. 3
      application-tenant/tenant-sync/src/main/java/apelet/tenantsync/online/service/SysTenantOnlCgreporHeadServiceImpl.java
  8. 22
      application-tenant/tenant-sync/src/main/java/apelet/tenantsync/online/service/SysTenantOnlFormHeadServiceImpl.java
  9. 132
      application-tenant/tenant-sync/src/main/java/apelet/tenantsync/online/service/SysTenantOnlineFormUpdateServiceImpl.java
  10. 2
      application/upms/upms-service/pom.xml
  11. 2
      common/common-flow-online/pom.xml
  12. 24
      common/common-generator/src/main/java/apelet/common/generator/controller/OnlFormHeadController.java
  13. 14
      common/common-online/src/main/java/apelet/common/online/controller/OnlineFormController.java
  14. 150
      common/common-online/src/main/java/apelet/common/online/util/TenantFieldUtil.java
  15. 4
      common/common-openapi/pom.xml
  16. 2
      common/common-tenant-online/pom.xml
  17. 12
      common/common-tenant/src/main/java/apelet/common/tenant/dao/SyncLogMapper.java
  18. 55
      common/common-tenant/src/main/java/apelet/common/tenant/model/SyncLog.java
  19. 79
      common/common-tenant/src/main/java/apelet/common/tenant/service/SyncLogService.java

2
application-tenant/tenant-admin/pom.xml

@ -104,7 +104,7 @@ @@ -104,7 +104,7 @@
<dependency>
<groupId>apelet</groupId>
<artifactId>common-generator</artifactId>
<version>1.0.5</version>
<version>1.0.6</version>
</dependency>
<dependency>

11
application-tenant/tenant-admin/src/main/java/apelet/tenantadmin/tenant/service/OnlineFormTenantSyncService.java

@ -1,10 +1,21 @@ @@ -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<Long> tenantIdSet);
/**
* 只同步表单 widgetJson(新增控件只追加)。
*/
void syncFormUpdate(Long pageId, Collection<Long> tenantIdSet);
/**
* 同步表单元数据与查询数据源。
*/
void syncFormMetadata(Collection<Long> pageIds, Collection<Long> tenantIdSet);
}

147
application-tenant/tenant-admin/src/main/java/apelet/tenantadmin/tenant/service/impl/OnlineFormTenantSyncServiceImpl.java

@ -16,6 +16,7 @@ import com.alibaba.fastjson.JSONObject; @@ -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; @@ -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 @@ -85,6 +87,16 @@ public class OnlineFormTenantSyncServiceImpl implements OnlineFormTenantSyncServ
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean syncUpdateTenant(Long pageId, List<Long> tenantIdSet) {
syncFormUpdate(pageId, tenantIdSet);
syncFormMetadata(Collections.singletonList(pageId), tenantIdSet);
return true;
}
@Override
public void syncFormUpdate(Long pageId, Collection<Long> tenantIdSet) {
if (pageId == null || CollUtil.isEmpty(tenantIdSet)) {
return;
}
List<OnlineForm> onlineFormListByPageIds = onlineFormService.getOnlineFormListByPageIds(Collections.singleton(pageId));
for (OnlineForm onlineForm : onlineFormListByPageIds) {
List<OnlineDatasource> datasourceList = onlineDatasourceService.getOnlineDatasourceListByFormIds(Collections.singleton(onlineForm.getFormId()));
@ -92,30 +104,8 @@ public class OnlineFormTenantSyncServiceImpl implements OnlineFormTenantSyncServ @@ -92,30 +104,8 @@ public class OnlineFormTenantSyncServiceImpl implements OnlineFormTenantSyncServ
.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 = parseRelativeFormCodes(widgetJson);
List<OnlCgreportHead> onlCgreportHeadList = relativeFormCodeList.isEmpty()
? Collections.emptyList()
: iOnlCgreportHeadService.list(new LambdaQueryWrapper<OnlCgreportHead>()
.in(OnlCgreportHead::getCode, relativeFormCodeList));
List<String> onlCgreportHeadIdList = onlCgreportHeadList.stream().map(OnlCgreportHead::getId).collect(Collectors.toList());
List<OnlCgreportItem> onlCgreportItemList = onlCgreportHeadIdList.isEmpty()
? Collections.emptyList()
: iOnlCgreportItemService.list(new LambdaQueryWrapper<OnlCgreportItem>()
.in(OnlCgreportItem::getCgrheadId, onlCgreportHeadIdList));
List<OnlCgreportParam> onlCgreportParamList = onlCgreportHeadIdList.isEmpty()
? Collections.emptyList()
: iOnlCgreportParamService.list(new LambdaQueryWrapper<OnlCgreportParam>()
.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 @@ -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<Long> pageIds, Collection<Long> tenantIdSet) {
if (CollUtil.isEmpty(pageIds) || CollUtil.isEmpty(tenantIdSet)) {
return;
}
// 1. 查询数据源:按表单 widgetJson 里的 relativeFormCode 反查
for (Long pageId : pageIds) {
List<OnlineForm> onlineFormList = onlineFormService.getOnlineFormListByPageIds(Collections.singleton(pageId));
for (OnlineForm onlineForm : onlineFormList) {
List<String> relativeFormCodeList = parseRelativeFormCodes(onlineForm.getWidgetJson());
List<OnlCgreportHead> onlCgreportHeadList = relativeFormCodeList.isEmpty()
? Collections.emptyList()
: iOnlCgreportHeadService.list(new LambdaQueryWrapper<OnlCgreportHead>()
.in(OnlCgreportHead::getCode, relativeFormCodeList));
List<String> onlCgreportHeadIdList = onlCgreportHeadList.stream().map(OnlCgreportHead::getId).collect(Collectors.toList());
List<OnlCgreportItem> onlCgreportItemList = onlCgreportHeadIdList.isEmpty()
? Collections.emptyList()
: iOnlCgreportItemService.list(new LambdaQueryWrapper<OnlCgreportItem>()
.in(OnlCgreportItem::getCgrheadId, onlCgreportHeadIdList));
List<OnlCgreportParam> onlCgreportParamList = onlCgreportHeadIdList.isEmpty()
? Collections.emptyList()
: iOnlCgreportParamService.list(new LambdaQueryWrapper<OnlCgreportParam>()
.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<OnlFormHead> headList = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(OnlFormHead.class), Collections.singleton(pageId));
if (CollUtil.isEmpty(headList)) {
continue;
}
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 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<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("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;
}
/**

134
application-tenant/tenant-admin/src/main/java/apelet/tenantadmin/tenant/service/impl/SysTenantRoleServiceImpl.java

@ -2,8 +2,6 @@ package apelet.tenantadmin.tenant.service.impl; @@ -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.*; @@ -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<SysTenantRole, Long> i @@ -75,32 +69,7 @@ public class SysTenantRoleServiceImpl extends BaseService<SysTenantRole, Long> 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<SysTenantRole, Long> i @@ -527,86 +496,14 @@ public class SysTenantRoleServiceImpl extends BaseService<SysTenantRole, Long> i
messageJsonData.toJSONString(),
TenantConstant.MESSAGE_QUEUE_SELECTOR_KEY);
//表单元数据与查询数据源新增同步
List<OnlineForm> onlineFormListByPageIds = onlineFormService.getOnlineFormListByPageIds((Set<Long>) pageIds);
for (OnlineForm onlineForm : onlineFormListByPageIds) {
// 根据表单json中的relativeFormCode获取到对应的查询数据源数据
String widgetJson = onlineForm.getWidgetJson();
List<String> relativeFormCodeList = parseRelativeFormCodes(widgetJson);
if (!relativeFormCodeList.isEmpty()) {
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 = onlCgreportHeadIdList.isEmpty()
? Collections.emptyList()
: iOnlCgreportItemService.list(new LambdaQueryWrapper<OnlCgreportItem>()
.in(OnlCgreportItem::getCgrheadId, onlCgreportHeadIdList));
List<OnlCgreportParam> onlCgreportParamList = onlCgreportHeadIdList.isEmpty()
? Collections.emptyList()
: iOnlCgreportParamService.list(new LambdaQueryWrapper<OnlCgreportParam>()
.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<OnlFormHead> headList = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(OnlFormHead.class), pageIds);
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 : 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<Long> changedTenantIds = resultTenantExtList.stream()
.map(SysTenantExt::getTenantId).collect(Collectors.toList());
onlineFormTenantSyncService.syncFormMetadata(pageIds, changedTenantIds);
}
}
private static @NotNull String getOnlFormHeadByPageIdSQL(Collection<Long> 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<SysTenantRole, Long> i @@ -752,25 +649,6 @@ public class SysTenantRoleServiceImpl extends BaseService<SysTenantRole, Long> i
return resultMenuIdSet;
}
/**
* 从 widgetJson 中提取所有的 relativeFormCode(查询数据源编码)。
* widgetJson 为空、非法 JSON 或没有该节点时返回空集合,不抛异常。
*/
private List<String> parseRelativeFormCodes(String widgetJson) {
if (StrUtil.isBlank(widgetJson)) {
return Collections.emptyList();
}
try {
List<String> 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

2
application-tenant/tenant-sync/pom.xml

@ -38,7 +38,7 @@ @@ -38,7 +38,7 @@
<dependency>
<groupId>apelet</groupId>
<artifactId>common-online</artifactId>
<version>1.0.6</version>
<version>1.0.7</version>
</dependency>
</dependencies>

69
application-tenant/tenant-sync/src/main/java/apelet/tenantsync/aop/DynamicTenantDatasourceAspect.java

@ -1,8 +1,12 @@ @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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;

3
application-tenant/tenant-sync/src/main/java/apelet/tenantsync/online/service/SysTenantOnlCgreporHeadServiceImpl.java

@ -7,6 +7,8 @@ import apelet.common.generator.service.IOnlCgreportHeadService; @@ -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 @@ -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)) {

22
application-tenant/tenant-sync/src/main/java/apelet/tenantsync/online/service/SysTenantOnlFormHeadServiceImpl.java

@ -5,6 +5,8 @@ import apelet.common.datasync.consumer.BaseDataSyncRocketMqConsumer; @@ -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; @@ -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 @@ -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 @@ -57,9 +60,24 @@ public class SysTenantOnlFormHeadServiceImpl implements BaseDataSyncConsumerServ
return;
}
//对需要同步的表单元数据进行多租户处理
// 先收集原始表名:sort 里存的是主表名,必须判断它是否在本次同步范围内,再决定是否加后缀
Set<String> 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) {
//判断对应租户是否已经存在相同名称的表单元数据

132
application-tenant/tenant-sync/src/main/java/apelet/tenantsync/online/service/SysTenantOnlineFormUpdateServiceImpl.java

@ -6,20 +6,27 @@ import apelet.common.online.dao.OnlineFormDatasourceMapper; @@ -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 @@ -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<Long> 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<OnlineForm> dbOnlineFormList = onlineFormMapper.selectList(new QueryWrapper<OnlineForm>().eq("form_code", onlineForm.getFormCode()));
OnlineForm onlineForm = adminFormJson.toJavaObject(OnlineForm.class);
JSONArray datasourceIdArray = messageJsonData.getJSONArray("datasourceIdSet");
List<Long> datasourceIds = datasourceIdArray == null ? Collections.emptyList() : datasourceIdArray.toJavaList(Long.class);
// 只处理本租户的表单,避免共享库下跨租户改写
List<OnlineForm> dbOnlineFormList = onlineFormMapper.selectList(new QueryWrapper<OnlineForm>()
.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<OnlineForm>().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<OnlineFormDatasource>()
.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<String, Object> 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<String> 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

2
application/upms/upms-service/pom.xml

@ -53,7 +53,7 @@ @@ -53,7 +53,7 @@
<dependency>
<groupId>apelet</groupId>
<artifactId>common-online</artifactId>
<version>1.0.6</version>
<version>1.0.7</version>
</dependency>
<dependency>
<groupId>apelet</groupId>

2
common/common-flow-online/pom.xml

@ -23,7 +23,7 @@ @@ -23,7 +23,7 @@
<dependency>
<groupId>apelet</groupId>
<artifactId>common-online</artifactId>
<version>1.0.6</version>
<version>1.0.7</version>
</dependency>
<dependency>
<groupId>apelet</groupId>

24
common/common-generator/src/main/java/apelet/common/generator/controller/OnlFormHeadController.java

@ -123,6 +123,14 @@ public class OnlFormHeadController { @@ -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 { @@ -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 { @@ -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;
}
}

14
common/common-online/src/main/java/apelet/common/online/controller/OnlineFormController.java

@ -35,6 +35,7 @@ import apelet.common.online.model.constant.FormType; @@ -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 { @@ -1150,6 +1151,19 @@ public class OnlineFormController {
List<OnlFormField> existingFields = onlFormFieldService.list(new LambdaQueryWrapper<OnlFormField>().eq(OnlFormField::getHeadId, head.getId()));
Map<String, OnlFormField> 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<String> newColumnNames = new ArrayList<>();
for (Map.Entry<String, JSONObject> columnEntry : columnWidgetMap.entrySet()) {
String columnName = columnEntry.getKey();

150
common/common-online/src/main/java/apelet/common/online/util/TenantFieldUtil.java

@ -0,0 +1,150 @@ @@ -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;
/**
* 租户自定义字段的命名约定。
* <p>
* A 端(租户端)新增的字段统一加 {@code tenant_} 前缀,与 B 端(管理端)下发的标准字段区分开;
* B 端新增字段时禁止使用该前缀(见 OnlineFormController 的校验)。
* <p>
* 字段名在各处统一小写(MetaCacheService、syncNewFieldsAndSubTables 均有 toLowerCase),
* 因此前缀判断前先归一化。
*
* @author chenchuchuan
*/
public final class TenantFieldUtil {
public static final String TENANT_FIELD_PREFIX = "tenant_";
/**
* 当前请求是否来自租户端(A端)。
* <p>
* 无 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} 的返回值。
* <p>
* 只改归属 {@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<String> 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<String> 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<String> 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() {
}
}

4
common/common-openapi/pom.xml

@ -31,13 +31,13 @@ @@ -31,13 +31,13 @@
<dependency>
<groupId>apelet</groupId>
<artifactId>common-online</artifactId>
<version>1.0.6</version>
<version>1.0.7</version>
</dependency>
<!-- 表单元数据:OnlFormHead / OnlFormField / IOnlFormHeadService,开放接口字段清单与关联字段属性换算依赖 -->
<dependency>
<groupId>apelet</groupId>
<artifactId>common-generator</artifactId>
<version>1.0.5</version>
<version>1.0.6</version>
</dependency>
<!-- token 会话存储(可吊销),复用 CommonRedisUtil -->
<dependency>

2
common/common-tenant-online/pom.xml

@ -23,7 +23,7 @@ @@ -23,7 +23,7 @@
<dependency>
<groupId>apelet</groupId>
<artifactId>common-online</artifactId>
<version>1.0.6</version>
<version>1.0.7</version>
</dependency>
</dependencies>
</project>

12
common/common-tenant/src/main/java/apelet/common/tenant/dao/SyncLogMapper.java

@ -0,0 +1,12 @@ @@ -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<SyncLog> {
}

55
common/common-tenant/src/main/java/apelet/common/tenant/model/SyncLog.java

@ -0,0 +1,55 @@ @@ -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;
}

79
common/common-tenant/src/main/java/apelet/common/tenant/service/SyncLogService.java

@ -0,0 +1,79 @@ @@ -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;
/**
* 租户同步日志服务。
* <p>
* 由 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<SyncLog> listRecent(Long tenantId, int limit) {
return syncLogMapper.selectList(new LambdaQueryWrapper<SyncLog>()
.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);
}
}
Loading…
Cancel
Save