Browse Source

feat(generator): 新增多选关联字段支持及流程任务分配优化

- 添加多选关联中间表类型定义(MetaType_Relation)及相应字段属性refMultiTableId
- 实现多选关联中间表的创建、同步和结构校验功能
- 重构表单字段服务以支持多选关联字段的双向关联查询
- 优化AutoSkipTaskListener中的任务分配人获取逻辑,支持表达式求值
- 增加流程任务监听器中表达式管理器的集成实现
- 完善ORM数据源工具中的关联表元数据递归解析防环机制
- 更新在线表单控制器以支持多选关联控件的中间表同步处理
feature/2026-08/0812-ccc-dev
chenchuchuan 3 days ago
parent
commit
93c691eade
  1. 41
      common/common-flow/src/main/java/apelet/common/flow/listener/AutoSkipTaskListener.java
  2. 2
      common/common-generator/pom.xml
  3. 18
      common/common-generator/src/main/java/apelet/common/generator/model/OnlFormField.java
  4. 7
      common/common-generator/src/main/java/apelet/common/generator/service/IOnlFormFieldService.java
  5. 10
      common/common-generator/src/main/java/apelet/common/generator/service/IOnlFormHeadService.java
  6. 68
      common/common-generator/src/main/java/apelet/common/generator/service/impl/OnlFormFieldServiceImpl.java
  7. 65
      common/common-generator/src/main/java/apelet/common/generator/service/impl/OnlFormHeadServiceImpl.java
  8. 5
      common/common-generator/src/main/java/apelet/common/generator/utils/CustomUtil.java
  9. 48
      common/common-generator/src/main/java/apelet/common/generator/utils/OrmGenDataSourceUtil.java
  10. 44
      common/common-generator/src/main/java/apelet/common/generator/utils/cache/MetaCacheService.java
  11. 2
      common/common-online/pom.xml
  12. 8
      common/common-online/src/main/java/apelet/common/online/abstractplugin/ListDataOrmService.java
  13. 288
      common/common-online/src/main/java/apelet/common/online/controller/OnlineFormController.java
  14. 39
      common/common-online/src/main/java/apelet/common/online/controller/OnlineOperationController.java
  15. 3
      common/common-online/src/main/java/apelet/common/online/service/impl/OnlineDatasourceServiceImpl.java
  16. 65
      common/common-online/src/main/java/apelet/common/online/service/impl/OnlineFormServiceImpl.java
  17. 10
      common/common-online/src/main/java/apelet/common/online/util/OrmOnlineDataSourceUtil.java
  18. 11
      common/common-online/src/main/java/apelet/common/online/util/WidgetFieldTypeMapping.java
  19. 217
      common/common-online/src/main/java/apelet/common/online/util/WidgetJsonUtil.java

41
common/common-flow/src/main/java/apelet/common/flow/listener/AutoSkipTaskListener.java

@ -16,7 +16,11 @@ import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.ExtensionAttribute; import org.flowable.bpmn.model.ExtensionAttribute;
import org.flowable.bpmn.model.UserTask; import org.flowable.bpmn.model.UserTask;
import org.flowable.common.engine.api.delegate.Expression;
import org.flowable.engine.delegate.TaskListener; import org.flowable.engine.delegate.TaskListener;
import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl;
import org.flowable.engine.impl.persistence.entity.ExecutionEntity;
import org.flowable.engine.impl.util.CommandContextUtil;
import org.flowable.task.api.Task; import org.flowable.task.api.Task;
import org.flowable.task.service.delegate.DelegateTask; import org.flowable.task.service.delegate.DelegateTask;
@ -56,7 +60,7 @@ public class AutoSkipTaskListener implements TaskListener {
UserTask userTask = flowApiService.getUserTask(t.getProcessDefinitionId(), t.getTaskDefinitionKey()); UserTask userTask = flowApiService.getUserTask(t.getProcessDefinitionId(), t.getTaskDefinitionKey());
List<ExtensionAttribute> attributes = userTask.getAttributes().get(FlowConstant.USER_TASK_AUTO_SKIP_KEY); List<ExtensionAttribute> attributes = userTask.getAttributes().get(FlowConstant.USER_TASK_AUTO_SKIP_KEY);
Set<String> skipTypes = new HashSet<>(StrUtil.split(attributes.get(0).getValue(), ",")); Set<String> skipTypes = new HashSet<>(StrUtil.split(attributes.get(0).getValue(), ","));
String assignedUser = this.getAssignedUser(userTask, t.getProcessDefinitionId(), t.getExecutionId()); String assignedUser = this.getAssignedUser(userTask, t);
if (StrUtil.isBlank(assignedUser)) { if (StrUtil.isBlank(assignedUser)) {
return; return;
} }
@ -108,22 +112,29 @@ public class AutoSkipTaskListener implements TaskListener {
comment.fillWith(t); comment.fillWith(t);
comment.setApprovalType(FlowApprovalType.AGREE); comment.setApprovalType(FlowApprovalType.AGREE);
comment.setTaskComment(StrFormatter.format("自动跳过审批。审批人 [{}], 跳过原因 [{}]。", comment.setTaskComment(StrFormatter.format("自动跳过审批。审批人 [{}], 跳过原因 [{}]。",
userTask.getAssignee(), this.getMessageBySkipType(skipType))); assignedUser, this.getMessageBySkipType(skipType)));
flowApiService.completeTask(t, comment, null, null); flowApiService.completeTask(t, comment, null, null);
} }
return comment != null; return comment != null;
} }
private String getAssignedUser(UserTask userTask, String processDefinitionId, String executionId) { private String getAssignedUser(UserTask userTask, DelegateTask t) {
String assignedUser = userTask.getAssignee(); // 引擎在触发create监听器之前,已对assignee配置的表达式完成求值(含${bean.method(execution)}形式的EL表达式),这里直接取任务上的实际处理人。
String assignedUser = t.getAssignee();
if (StrUtil.isNotBlank(assignedUser)) { if (StrUtil.isNotBlank(assignedUser)) {
if (assignedUser.startsWith("${") && assignedUser.endsWith("}")) { return assignedUser;
String variableName = assignedUser.substring(2, assignedUser.length() - 1); }
assignedUser = flowApiService.getExecutionVariableStringWithSafe(executionId, variableName); String modelAssignee = userTask.getAssignee();
if (StrUtil.isNotBlank(modelAssignee)) {
if (modelAssignee.startsWith("${") && modelAssignee.endsWith("}")) {
// 模型中配置的是EL表达式(简单变量或Bean方法调用两种形式),统一交给引擎的表达式管理器求值。
assignedUser = this.getExpressionValue(modelAssignee, t.getExecutionId());
} else {
assignedUser = modelAssignee;
} }
} else { } else {
FlowTaskExt flowTaskExt = flowTaskExtService FlowTaskExt flowTaskExt = flowTaskExtService
.getByProcessDefinitionIdAndTaskId(processDefinitionId, userTask.getId()); .getByProcessDefinitionIdAndTaskId(t.getProcessDefinitionId(), userTask.getId());
List<String> candidateUsernames; List<String> candidateUsernames;
if (StrUtil.isBlank(flowTaskExt.getCandidateUsernames())) { if (StrUtil.isBlank(flowTaskExt.getCandidateUsernames())) {
candidateUsernames = Collections.emptyList(); candidateUsernames = Collections.emptyList();
@ -131,7 +142,7 @@ public class AutoSkipTaskListener implements TaskListener {
candidateUsernames = StrUtil.split(flowTaskExt.getCandidateUsernames(), ","); candidateUsernames = StrUtil.split(flowTaskExt.getCandidateUsernames(), ",");
} else { } else {
String value = flowApiService String value = flowApiService
.getExecutionVariableStringWithSafe(executionId, FlowConstant.TASK_APPOINTED_ASSIGNEE_VAR); .getExecutionVariableStringWithSafe(t.getExecutionId(), FlowConstant.TASK_APPOINTED_ASSIGNEE_VAR);
candidateUsernames = value == null ? null : StrUtil.split(value, ","); candidateUsernames = value == null ? null : StrUtil.split(value, ",");
} }
if (candidateUsernames != null && candidateUsernames.size() == 1) { if (candidateUsernames != null && candidateUsernames.size() == 1) {
@ -141,6 +152,18 @@ public class AutoSkipTaskListener implements TaskListener {
return assignedUser; return assignedUser;
} }
private String getExpressionValue(String expressionStr, String executionId) {
ProcessEngineConfigurationImpl engineConfiguration = CommandContextUtil.getProcessEngineConfiguration();
ExecutionEntity execution = CommandContextUtil.getExecutionEntityManager().findById(executionId);
if (execution == null) {
return null;
}
// 与引擎UserTaskActivityBehavior.handleAssignments的求值方式保持一致,execution关键字可正常绑定到当前执行流对象。
Expression expression = engineConfiguration.getExpressionManager().createExpression(expressionStr);
Object value = expression.getValue(execution);
return value == null ? null : value.toString();
}
private String getMessageBySkipType(String skipType) { private String getMessageBySkipType(String skipType) {
switch (skipType) { switch (skipType) {
case EQ_PREV_SUBMIT_USER: case EQ_PREV_SUBMIT_USER:

2
common/common-generator/pom.xml

@ -59,7 +59,7 @@
<dependency> <dependency>
<groupId>apelet</groupId> <groupId>apelet</groupId>
<artifactId>common-orm</artifactId> <artifactId>common-orm</artifactId>
<version>1.0.3</version> <version>1.0.4</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency> <dependency>

18
common/common-generator/src/main/java/apelet/common/generator/model/OnlFormField.java

@ -88,6 +88,12 @@ public class OnlFormField extends BaseModel implements Serializable {
@Schema(description = "eas 字段标识") @Schema(description = "eas 字段标识")
private String easFieldMark; private String easFieldMark;
/**
* 多选关联中间表头id404 多选关联字段专用指向中间表的 onl_form_head.id为空表示非多选字段
*/
@Schema(description = "多选关联中间表头id")
private Long refMultiTableId;
@ -100,6 +106,18 @@ public class OnlFormField extends BaseModel implements Serializable {
@TableField(exist = false) @TableField(exist = false)
private Map<String, Object> refPropertMap; private Map<String, Object> refPropertMap;
/**
* 多选关联中间表的描述信息结构与 refPropertMap 一致id = 中间表 onl_form_head.idtable_name = 中间表物理名供表单元数据详情回显
*/
@RelationDict(
masterIdField = "refMultiTableId",
equalOneToOneRelationField = "onlFormHead",
slaveModelClass = OnlFormHead.class,
slaveIdField = "id",
slaveNameField = "tableTxt")
@TableField(exist = false)
private Map<String, Object> refMultiTableMap;
@RelationGlobalDict( @RelationGlobalDict(
masterIdField = "gradeId", masterIdField = "gradeId",
dictCode = "onl_fieldType") dictCode = "onl_fieldType")

7
common/common-generator/src/main/java/apelet/common/generator/service/IOnlFormFieldService.java

@ -25,6 +25,13 @@ public interface IOnlFormFieldService extends IService<OnlFormField> {
*/ */
List<JSONObject> selectFormFieldSlaveByHead(Map<String, Object> map); List<JSONObject> selectFormFieldSlaveByHead(Map<String, Object> map);
/**
* 查询表字段和字段关联的多选中间表名404 多选关联字段专用 ref_multi_table_id 关联
* @param map
* @return
*/
List<JSONObject> selectMultiTableSlaveByHead(Map<String, Object> map);
List<OnlFormField> queryOnlFormField(LambdaQueryWrapper lambdaQueryWrapper); List<OnlFormField> queryOnlFormField(LambdaQueryWrapper lambdaQueryWrapper);
List<OnlFormField> getFiledByTableId(String key); List<OnlFormField> getFiledByTableId(String key);

10
common/common-generator/src/main/java/apelet/common/generator/service/IOnlFormHeadService.java

@ -130,4 +130,14 @@ public interface IOnlFormHeadService extends IService<OnlFormHead> {
* @return OnlFormHead * @return OnlFormHead
*/ */
OnlFormHead createMetaAndSync(String tableName, String tableTxt, String metaType, String sortFiled); OnlFormHead createMetaAndSync(String tableName, String tableTxt, String metaType, String sortFiled);
/**
* 创建多选关联中间表并同步数据库
* <p>中间表结构固定为 biz_id/obj_id 联合主键 + seq id orm MultiLink 链路按 biz_id/obj_id/seq 三列读写
* 与通用 {@link #syncDB} 的建表模板硬编码 id 主键不兼容故单独建表后复用同步收尾逻辑</p>
*
* @param onlFormHead 已落库的中间表表头需含 id/tableName/tableTxt
* @return 同步成功返回 true
*/
Boolean syncRelationTable(OnlFormHead onlFormHead);
} }

68
common/common-generator/src/main/java/apelet/common/generator/service/impl/OnlFormFieldServiceImpl.java

@ -3,7 +3,6 @@ package apelet.common.generator.service.impl;
import apelet.common.core.cache.CacheKey; import apelet.common.core.cache.CacheKey;
import apelet.common.generator.dao.OnlFormFieldMapper; import apelet.common.generator.dao.OnlFormFieldMapper;
import apelet.common.generator.model.OnlFormField; import apelet.common.generator.model.OnlFormField;
import apelet.common.generator.model.OnlFormHead;
import apelet.common.generator.service.IOnlFormFieldService; import apelet.common.generator.service.IOnlFormFieldService;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
@ -14,7 +13,10 @@ import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.*; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
@ -49,27 +51,63 @@ public class OnlFormFieldServiceImpl extends ServiceImpl<OnlFormFieldMapper, Onl
} }
@Override @Override
public List<JSONObject> selectMultiTableSlaveByHead(Map<String, Object> map) {
// 404 多选关联字段专用:按 ref_multi_table_id 关联其专用中间表(与 ref_propert 的 402 语义相互独立)
String sql = "SELECT fh.table_name,ff.ref_multi_table_id FROM onl_form_field ff INNER JOIN onl_form_head fh ON ff.ref_multi_table_id=fh.id WHERE ff.head_id='"+map.get("headId")+"'";
return onlFormFieldMapper.excuteSQL(sql);
}
@Override
public List<OnlFormField> queryOnlFormField(LambdaQueryWrapper lambdaQueryWrapper) { public List<OnlFormField> queryOnlFormField(LambdaQueryWrapper lambdaQueryWrapper) {
List<OnlFormField> onlFormFieldList = this.list(lambdaQueryWrapper); List<OnlFormField> onlFormFieldList = this.list(lambdaQueryWrapper);
if (onlFormFieldList == null || onlFormFieldList.isEmpty()) {
return onlFormFieldList;
}
Map<String, Object> map = new HashMap<String, Object>(); Map<String, Object> map = new HashMap<String, Object>();
map.put("headId",onlFormFieldList.get(0).getHeadId()); map.put("headId", onlFormFieldList.get(0).getHeadId());
List<JSONObject> list = this.selectFormFieldSlaveByHead(map); // 两种关联语义各查一次:402 关联表 / 404 多选中间表
for(OnlFormField onlFormField:onlFormFieldList){ Map<Long, String> refPropertTableNameMap = this.toTableNameMap(this.selectFormFieldSlaveByHead(map), "ref_propert");
if(onlFormField.getRefPropert()!=null){ Map<Long, String> refMultiTableNameMap = this.toTableNameMap(this.selectMultiTableSlaveByHead(map), "ref_multi_table_id");
for(JSONObject jsonObject:list){ for (OnlFormField onlFormField : onlFormFieldList) {
if(jsonObject.getLong("ref_propert").equals(onlFormField.getRefPropert())){ // 402 单选关联:关联表描述
if (onlFormField.getRefPropert() != null && refPropertTableNameMap.containsKey(onlFormField.getRefPropert())) {
Map<String, Object> map1 = new HashMap<>(); Map<String, Object> refPropertMap = new HashMap<>();
map1.put("table_name", jsonObject.get("table_name")); refPropertMap.put("table_name", refPropertTableNameMap.get(onlFormField.getRefPropert()));
map1.put("id",jsonObject.getLong("ref_propert")); refPropertMap.put("id", onlFormField.getRefPropert());
onlFormField.setRefPropertMap(map1); onlFormField.setRefPropertMap(refPropertMap);
} }
} // 404 多选关联:中间表描述(结构与 refPropertMap 一致,id = 中间表 onl_form_head.id)
if (onlFormField.getRefMultiTableId() != null && refMultiTableNameMap.containsKey(onlFormField.getRefMultiTableId())) {
Map<String, Object> refMultiTableMap = new HashMap<>();
refMultiTableMap.put("table_name", refMultiTableNameMap.get(onlFormField.getRefMultiTableId()));
refMultiTableMap.put("id", onlFormField.getRefMultiTableId());
onlFormField.setRefMultiTableMap(refMultiTableMap);
} }
} }
return onlFormFieldList; return onlFormFieldList;
} }
/**
* 关联表头id 表名 的映射避免嵌套遍历
*
* @param list selectFormFieldSlaveByHead / selectMultiTableSlaveByHead 的查询结果
* @param refColumnName 关联字段所在列名ref_propert ref_multi_table_id
* @return 关联表头id 表名
*/
private Map<Long, String> toTableNameMap(List<JSONObject> list, String refColumnName) {
Map<Long, String> tableNameMap = new HashMap<>();
if (list == null || list.isEmpty()) {
return tableNameMap;
}
for (JSONObject jsonObject : list) {
Long refId = jsonObject.getLong(refColumnName);
if (refId != null) {
tableNameMap.put(refId, jsonObject.getString("table_name"));
}
}
return tableNameMap;
}
@Override @Override
public List<OnlFormField> getFiledByTableId(String key) { public List<OnlFormField> getFiledByTableId(String key) {
String sql = "SELECT ff.* FROM onl_form_head fh " + String sql = "SELECT ff.* FROM onl_form_head fh " +

65
common/common-generator/src/main/java/apelet/common/generator/service/impl/OnlFormHeadServiceImpl.java

@ -74,6 +74,16 @@ public class OnlFormHeadServiceImpl extends ServiceImpl<OnlFormHeadMapper, OnlFo
*/ */
private static final int DEFAULT_CACHED_TABLE_HOURS = 168; private static final int DEFAULT_CACHED_TABLE_HOURS = 168;
/**
* 建表 SQL 尾部引擎/字符集/排序规则/行格式/注释前缀createSql 与中间表建表共用避免两处不一致
*/
private static final String TABLE_TAIL_SQL = " ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=DYNAMIC COMMENT=";
/**
* 多选关联中间表的约定列集合有且仅有这三列 id无审计列
*/
private static final Set<String> RELATION_TABLE_COLUMNS = new HashSet<>(Arrays.asList("biz_id", "obj_id", "seq"));
public static Map<String, String> typeMap = new HashMap<>(); public static Map<String, String> typeMap = new HashMap<>();
static { static {
@ -297,6 +307,16 @@ public class OnlFormHeadServiceImpl extends ServiceImpl<OnlFormHeadMapper, OnlFo
} }
} }
} }
return this.finishSyncDB(onlFormHead);
}
/**
* 同步收尾更新同步状态清理缓存回写表头syncDB 与中间表建表共用
*
* @param onlFormHead 表头
* @return 回写是否成功
*/
private Boolean finishSyncDB(OnlFormHead onlFormHead) {
//更新同步状态 //更新同步状态
onlFormHead.setStatus("1"); onlFormHead.setStatus("1");
onlFormFieldService.update(new LambdaUpdateWrapper<OnlFormField>() onlFormFieldService.update(new LambdaUpdateWrapper<OnlFormField>()
@ -312,6 +332,49 @@ public class OnlFormHeadServiceImpl extends ServiceImpl<OnlFormHeadMapper, OnlFo
return this.updateById(onlFormHead); return this.updateById(onlFormHead);
} }
@Override
public Boolean syncRelationTable(OnlFormHead onlFormHead) {
String tableName = onlFormHead.getTableName();
// 中间表结构固定,不做"已存在则 ALTER 补列"的增量同步:结构不符就直接重建,
// 避免出现"表在但结构是旧的"导致后续读写列对不上。
if (DbTableUtil.judgeTableIsExit(tableName, username)) {
if (this.hasRelationTableColumns(tableName)) {
return this.finishSyncDB(onlFormHead);
}
log.warn("中间表结构已变化,执行重建。tableName={}", tableName);
jdbcTemplate.execute("DROP TABLE IF EXISTS `" + tableName + "`");
}
// 中间表由 orm 的 MultiLink 链路读写,只写 biz_id/obj_id/seq 三列且不回填行主键,
// 故不建 id 列,改用 biz_id+obj_id 联合主键(顺带在库层约束"同一宿主行不重复选同一条记录");
// 亦不带审计列(jar 只认这三列,审计列永远为空,无意义)。
String sql = "CREATE TABLE `" + tableName + "` (" +
" `biz_id` bigint NOT NULL COMMENT '主表业务ID(宿主行id)'," +
" `obj_id` bigint NOT NULL COMMENT '关联表ID(所选记录主键)'," +
" `seq` int DEFAULT NULL COMMENT '排序顺序'," +
" PRIMARY KEY (`biz_id`,`obj_id`)" +
")" + TABLE_TAIL_SQL + "'" + onlFormHead.getTableTxt() + "';";
log.info("生成的中间表sql为: " + sql);
try {
jdbcTemplate.execute(sql);
} catch (Exception e) {
log.error("同步中间表数据库发生报错" + e.getMessage(), e);
throw new RuntimeException("同步中间表数据库发生报错" + e.getMessage());
}
return this.finishSyncDB(onlFormHead);
}
/**
* 校验已存在的中间表结构是否为当前约定有且仅有 biz_id / obj_id / seq 三列
* 多列历史结构带 id 或审计列或少列都视为结构不符需重建否则 orm 按三列读写会报未知列/缺主键值
*
* @param tableName 中间表物理名
* @return 结构符合返回 true
*/
private boolean hasRelationTableColumns(String tableName) {
Set<String> databaseColumn = this.getDbMetaColumns(tableName);
return databaseColumn.size() == RELATION_TABLE_COLUMNS.size() && databaseColumn.containsAll(RELATION_TABLE_COLUMNS);
}
/** /**
* 更细表结构 * 更细表结构
* *
@ -1358,7 +1421,7 @@ public class OnlFormHeadServiceImpl extends ServiceImpl<OnlFormHeadMapper, OnlFo
} }
} }
//数据库 //数据库
sb.append(") ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=DYNAMIC COMMENT='" + onlFormHead.getTableTxt() + "';"); sb.append(")" + TABLE_TAIL_SQL + "'" + onlFormHead.getTableTxt() + "';");
//去除掉多余的逗号 //去除掉多余的逗号
return sb.toString().replaceAll(",,", ",").replaceAll(",,,", ","); return sb.toString().replaceAll(",,", ",").replaceAll(",,,", ",");
} }

5
common/common-generator/src/main/java/apelet/common/generator/utils/CustomUtil.java

@ -19,6 +19,11 @@ public class CustomUtil {
*/ */
public final static String MetaType_Single = "2"; public final static String MetaType_Single = "2";
/**
* 多选关联中间表
*/
public final static String MetaType_Relation = "3";
public final static String FieldType_varchar = "0"; public final static String FieldType_varchar = "0";
public final static String FieldType_int = "1"; public final static String FieldType_int = "1";
public final static String FieldType_bigint = "2"; public final static String FieldType_bigint = "2";

48
common/common-generator/src/main/java/apelet/common/generator/utils/OrmGenDataSourceUtil.java

@ -55,35 +55,67 @@ public class OrmGenDataSourceUtil extends OrmDataSourceNewUtil {
@Override @Override
public EntityTable getEntityTable(String datasourceVariableName) { public EntityTable getEntityTable(String datasourceVariableName) {
return getEntityTable(datasourceVariableName, new HashSet<String>());
}
/**
* 获取实体元数据并补全各类关联的 superTable
* <p>元数据来自 {@link MetaCacheService}按表名缓存对象共享可变因此此处补挂的 superTable 会留在缓存对象上
* 后续调用可复用orm 内部会直接取 LinkEntityColumn / MultiLinkEntityColumn superTable 使用
* 若为 null 会在 ImplUtils 解析结果集时抛 NPE</p>
*
* @param datasourceVariableName 表物理名
* @param visiting 当前递归链上已进入的表用于防环
*/
private EntityTable getEntityTable(String datasourceVariableName, Set<String> visiting) {
try { try {
EntityTable entityTable = getEntityTable(true,datasourceVariableName,0); return getEntityTable(false, datasourceVariableName, 0, visiting);
return entityTable;
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
/**
* 多选关联中间表MultiLinkEntityColumn的最大解析深度防止元数据配错导致递归失控
*/
private static final int MULTI_LINK_MAX_LEVEL = 3;
private EntityTable getEntityTable(boolean ismain, String datasourceVariableName, int level, Set<String> visiting) throws IOException, ClassNotFoundException {
private EntityTable getEntityTable(boolean ismain,String datasourceVariableName, int level) throws IOException, ClassNotFoundException { // 防环:同一递归链上重复进入同一张表说明元数据成环,直接跳过补全
if (!visiting.add(datasourceVariableName)) {
return (EntityTable) metaCacheService.getData(datasourceVariableName);
}
EntityTable entityTable = (EntityTable) metaCacheService.getData(datasourceVariableName); EntityTable entityTable = (EntityTable) metaCacheService.getData(datasourceVariableName);
List<EntityColumn> entityColumnList = entityTable.getEntityColumnList(); List<EntityColumn> entityColumnList = entityTable.getEntityColumnList();
for (EntityColumn entityColumn : entityColumnList) { for (EntityColumn entityColumn : entityColumnList) {
// 多选关联字段(404):superTable 已由 MetaCacheService 按 refPropert 指向 F7 目标表,
// 此处补全为完整元数据,使目标表自身的链接列(F7/分录)也带上 superTable
if (entityColumn instanceof MultiLinkEntityColumn) {
MultiLinkEntityColumn multiLinkEntityColumn = (MultiLinkEntityColumn) entityColumn;
EntityTable targetTable = multiLinkEntityColumn.getSuperTable();
if (targetTable != null && level < MULTI_LINK_MAX_LEVEL && StringUtils.isNotEmpty(targetTable.getTableName())) {
multiLinkEntityColumn.setSuperTable(getEntityTable(false, targetTable.getTableName(), level + 1, visiting));
}
continue;
}
if (entityColumn instanceof LinkEntityColumn) { if (entityColumn instanceof LinkEntityColumn) {
LinkEntityColumn linkEntityColumn = (LinkEntityColumn) entityColumn; LinkEntityColumn linkEntityColumn = (LinkEntityColumn) entityColumn;
if (linkEntityColumn.getEntityLinkRelation().getRelationType() == 1) {//一对多 if (linkEntityColumn.getEntityLinkRelation().getRelationType() == 1) {//一对多
if (level < 2) { if (level < 2) {
if (ismain) { // 主表这一层保持原有行为(level 归 0);非主表(分录表/中间表自身的子表)按真实深度递归,
linkEntityColumn.getEntityLinkRelation().setSuperTable(getEntityTable(false, linkEntityColumn.getEntityLinkRelation().getRelationName(), 0)); // 否则其子表的 superTable 永远是空壳,orm 读取时会 NPE
} int childLevel = ismain ? 0 : level + 1;
linkEntityColumn.getEntityLinkRelation().setSuperTable(getEntityTable(false, linkEntityColumn.getEntityLinkRelation().getRelationName(), childLevel, visiting));
} }
} else if (level < 2) { } else if (level < 2) {
linkEntityColumn.getEntityLinkRelation().setSuperTable(getEntityTable(false, linkEntityColumn.getEntityLinkRelation().getRelationName(), level + 1)); linkEntityColumn.getEntityLinkRelation().setSuperTable(getEntityTable(false, linkEntityColumn.getEntityLinkRelation().getRelationName(), level + 1, visiting));
} }
} }
} }
// 遍历结束出栈
visiting.remove(datasourceVariableName);
// EntityTable entityTable = new EntityTable(); // EntityTable entityTable = new EntityTable();
// List<OnlFormField> onlFormFieldList = onlFormHeadService.onlFormFieldListCache(onlFormHead.getId()); // List<OnlFormField> onlFormFieldList = onlFormHeadService.onlFormFieldListCache(onlFormHead.getId());
// entityTable.setTableId(onlFormHead.getId()); // entityTable.setTableId(onlFormHead.getId());

44
common/common-generator/src/main/java/apelet/common/generator/utils/cache/MetaCacheService.java vendored

@ -8,10 +8,7 @@ import apelet.common.generator.model.OnlFormField;
import apelet.common.generator.model.OnlFormHead; import apelet.common.generator.model.OnlFormHead;
import apelet.common.generator.service.IOnlFormFieldService; import apelet.common.generator.service.IOnlFormFieldService;
import apelet.common.generator.service.IOnlFormHeadService; import apelet.common.generator.service.IOnlFormHeadService;
import apelet.common.orm.impl.EntityColumn; import apelet.common.orm.impl.*;
import apelet.common.orm.impl.EntityLinkRelation;
import apelet.common.orm.impl.EntityTable;
import apelet.common.orm.impl.LinkEntityColumn;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.redisson.api.RBucket; import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient; import org.redisson.api.RedissonClient;
@ -100,6 +97,20 @@ public class MetaCacheService {
return object; return object;
} }
/**
* 获取指定表的实体元数据把受检异常收敛为运行时异常便于在实体装配过程中直接调用
*
* @param tableName 表物理名
* @return 实体元数据
*/
private EntityTable getEntityTableFromCache(String tableName) {
try {
return (EntityTable) this.getData(tableName);
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException("获取实体元数据失败,tableName=" + tableName, e);
}
}
@ -111,10 +122,25 @@ public class MetaCacheService {
entityTable.setTableName(onlFormHead.getTableName()); entityTable.setTableName(onlFormHead.getTableName());
entityTable.setModelName(onlFormHead.getTableName()); entityTable.setModelName(onlFormHead.getTableName());
for (int i = 0; i < onlFormFieldList.size(); i++) { for (OnlFormField onlFormField : onlFormFieldList) {
OnlFormField onlFormField = onlFormFieldList.get(i);
EntityColumn entityColumn = null; EntityColumn entityColumn = null;
if (onlFormField.getRefPropert() != null) { if (onlFormField.getRefMultiTableId() != null) {
// 404 多选关联字段:集合语义。
// middleTableName = 中间表(orm 按 biz_id/obj_id/seq 三列读写中间表);
// superTable = F7 关联目标表(取自 refPropert),orm 按 id 反查目标表把所选记录整行带出。
// 两者缺一不可:MultiLinkEntityColumn#validate 要求均非空。
OnlFormHead relationHead = onlFormHeadService.getById(onlFormField.getRefMultiTableId());
MultiLinkEntityColumn multiLinkEntityColumn = new MultiLinkEntityColumn();
multiLinkEntityColumn.setMiddleTableName(relationHead == null ? null : relationHead.getTableName());
if (onlFormField.getRefPropert() != null) {
OnlFormHead targetHead = onlFormHeadService.getOnlFormHeadFromCache(onlFormField.getRefPropert());
// 排除自引用,避免 getData 递归自身(缓存尚未写入,会无限递归)
if (targetHead != null && !datasourceVariableName.equalsIgnoreCase(targetHead.getTableName())) {
multiLinkEntityColumn.setSuperTable(this.getEntityTableFromCache(targetHead.getTableName()));
}
}
entityColumn = multiLinkEntityColumn;
} else if (onlFormField.getRefPropert() != null) {
OnlFormHead childFormHead = onlFormHeadService.getOnlFormHeadFromCache(onlFormField.getRefPropert()); OnlFormHead childFormHead = onlFormHeadService.getOnlFormHeadFromCache(onlFormField.getRefPropert());
entityColumn = new LinkEntityColumn(); entityColumn = new LinkEntityColumn();
EntityLinkRelation entityLinkRelation = new EntityLinkRelation(); EntityLinkRelation entityLinkRelation = new EntityLinkRelation();
@ -136,8 +162,8 @@ public class MetaCacheService {
dictFilter.setItemId(onlFormField.getFieldType()); dictFilter.setItemId(onlFormField.getFieldType());
GlobalDictItem globalDictItem = globalDictItemService.getOne(dictFilter); GlobalDictItem globalDictItem = globalDictItemService.getOne(dictFilter);
entityColumn.setColumnType(JDBCType.valueOf( entityColumn.setColumnType(JDBCType.valueOf(
globalDictItem.getItemName().toUpperCase().equalsIgnoreCase("DATETIME") globalDictItem.getItemName().equalsIgnoreCase("DATETIME")
?"TIMESTAMP":globalDictItem.getItemName().toUpperCase() ? "TIMESTAMP" : globalDictItem.getItemName().toUpperCase()
).getVendorTypeNumber()); ).getVendorTypeNumber());
entityTable.getEntityColumnList().add(entityColumn); entityTable.getEntityColumnList().add(entityColumn);
} }

2
common/common-online/pom.xml

@ -76,7 +76,7 @@
<dependency> <dependency>
<groupId>apelet</groupId> <groupId>apelet</groupId>
<artifactId>common-orm</artifactId> <artifactId>common-orm</artifactId>
<version>1.0.3</version> <version>1.0.4</version>
</dependency> </dependency>
</dependencies> </dependencies>

8
common/common-online/src/main/java/apelet/common/online/abstractplugin/ListDataOrmService.java

@ -13,6 +13,7 @@ import apelet.common.online.model.constant.FieldFilterType;
import apelet.common.online.model.constant.RelationType; import apelet.common.online.model.constant.RelationType;
import apelet.common.orm.impl.*; import apelet.common.orm.impl.*;
import apelet.common.orm.parser.FilterParser; import apelet.common.orm.parser.FilterParser;
import apelet.common.online.util.WidgetFieldTypeMapping;
import apelet.common.online.util.WidgetJsonUtil; import apelet.common.online.util.WidgetJsonUtil;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
@ -386,6 +387,13 @@ public class ListDataOrmService extends ListDataService {
OnlineColumn onlineColumn = columnMap.get(columnId); OnlineColumn onlineColumn = columnMap.get(columnId);
if (onlineColumn == null) return; if (onlineColumn == null) return;
if (data.containsKey("slaveTableId")) { if (data.containsKey("slaveTableId")) {
// 关联多选(404):集合以绑定字段名为 key 挂在宿主对象上,selector 只需裸列名,
// 不走 F7 的 「.id / .displayField」拼法
Integer widgetType = jsonObject.getInteger("widgetType");
if (widgetType != null && widgetType == WidgetFieldTypeMapping.WIDGET_TYPE_MULTI_RELATION) {
selectorSet.add(onlineColumn.getColumnName().toLowerCase());
return;
}
// F7 字段 // F7 字段
String string = jsonObject.getJSONObject("props").getJSONObject("relativeTable").getString("displayField"); String string = jsonObject.getJSONObject("props").getJSONObject("relativeTable").getString("displayField");
selectorSet.add(onlineColumn.getColumnName().toLowerCase() + "." + "id"); selectorSet.add(onlineColumn.getColumnName().toLowerCase() + "." + "id");

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

@ -16,6 +16,7 @@ import apelet.common.core.util.MyModelUtil;
import apelet.common.core.validator.UpdateGroup; import apelet.common.core.validator.UpdateGroup;
import apelet.common.dbutil.object.SqlTable; import apelet.common.dbutil.object.SqlTable;
import apelet.common.dbutil.object.SqlTableColumn; import apelet.common.dbutil.object.SqlTableColumn;
import apelet.common.generator.dto.OnlFormHeadDto;
import apelet.common.generator.model.OnlFormField; import apelet.common.generator.model.OnlFormField;
import apelet.common.generator.model.OnlFormHead; import apelet.common.generator.model.OnlFormHead;
import apelet.common.generator.service.IOnlFormFieldService; import apelet.common.generator.service.IOnlFormFieldService;
@ -38,6 +39,7 @@ import apelet.common.online.util.WidgetFieldTypeMapping;
import apelet.common.online.util.WidgetJsonUtil; import apelet.common.online.util.WidgetJsonUtil;
import apelet.common.online.vo.OnlineFormVo; import apelet.common.online.vo.OnlineFormVo;
import apelet.common.sequence.wrapper.IdGeneratorWrapper; import apelet.common.sequence.wrapper.IdGeneratorWrapper;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
@ -290,6 +292,8 @@ public class OnlineFormController {
onlineFormDto.setWidgetJson(updatedJson.toJSONString()); onlineFormDto.setWidgetJson(updatedJson.toJSONString());
} }
} }
// ★ 多选关联(404):创建/复用中间表并回写绑定字段(前置:锚点字段已由 syncNewFieldsAndSubTables 创建,字段不存在则不处理)
syncMultiRelationTables(onlineForm, dataSource);
if (!onlineFormService.update(onlineForm, originalOnlineForm, datasourceIdSet)) { if (!onlineFormService.update(onlineForm, originalOnlineForm, datasourceIdSet)) {
redissonClient.getBucket(OnlineRedisKeyUtil.makeOnlineFormKey(onlineForm.getFormId())).delete(); redissonClient.getBucket(OnlineRedisKeyUtil.makeOnlineFormKey(onlineForm.getFormId())).delete();
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
@ -736,9 +740,8 @@ public class OnlineFormController {
field.setCreateUserId(tokenData.getUserId()); field.setCreateUserId(tokenData.getUserId());
field.setUpdateTime(now); field.setUpdateTime(now);
field.setUpdateUserId(tokenData.getUserId()); field.setUpdateUserId(tokenData.getUserId());
onlineQuickFormFieldService.save(field);
} }
onlineQuickFormFieldService.saveBatch(fieldList);
} }
// 3. 解析数据源与主表 // 3. 解析数据源与主表
OnlineDatasource datasource = onlineDatasourceService.getOnlineDatasourceByMasterTableId(dto.getMasterTableId()); OnlineDatasource datasource = onlineDatasourceService.getOnlineDatasourceByMasterTableId(dto.getMasterTableId());
@ -849,12 +852,17 @@ public class OnlineFormController {
if (CollUtil.isEmpty(fieldList) || columnIdMap == null || columnIdMap.isEmpty()) { if (CollUtil.isEmpty(fieldList) || columnIdMap == null || columnIdMap.isEmpty()) {
return; return;
} }
List<OnlineQuickFormField> updateList = new ArrayList<>();
for (OnlineQuickFormField field : fieldList) { for (OnlineQuickFormField field : fieldList) {
Long columnId = columnIdMap.get(field.getFieldName().toLowerCase()); Long columnId = columnIdMap.get(field.getFieldName().toLowerCase());
if (columnId != null && !Objects.equals(columnId, field.getColumnId())) { if (columnId == null || Objects.equals(columnId, field.getColumnId())) {
field.setColumnId(columnId); continue;
onlineQuickFormFieldService.updateById(field);
} }
field.setColumnId(columnId);
updateList.add(field);
}
if (CollUtil.isNotEmpty(updateList)) {
onlineQuickFormFieldService.updateBatchById(updateList);
} }
} }
@ -865,19 +873,48 @@ public class OnlineFormController {
if (CollUtil.isEmpty(fieldList)) { if (CollUtil.isEmpty(fieldList)) {
return; return;
} }
// 先收集"是过滤条件且有过滤方案"的字段(同名字段后者覆盖,与原逐条更新语义一致),避免逐字段查列
Map<String, Integer> fieldFilterTypeMap = new HashMap<>();
for (OnlineQuickFormField field : fieldList) { for (OnlineQuickFormField field : fieldList) {
if (field.getIsFilter() == null || field.getIsFilter() != 1 || field.getFilterType() == null || field.getFilterType() == FieldFilterType.NO_FILTER) { if (field.getIsFilter() == null || field.getIsFilter() != 1 || field.getFilterType() == null || field.getFilterType() == FieldFilterType.NO_FILTER) {
continue; continue;
} }
OnlineColumn column = onlineColumnService.getOnlineColumnByTableIdAndColumnName(masterTableId, field.getFieldName()); if (StringUtils.isBlank(field.getFieldName())) {
if (column != null && !Objects.equals(column.getFilterType(), field.getFilterType())) { continue;
column.setFilterType(field.getFilterType()); }
column.setUpdateTime(new Date()); fieldFilterTypeMap.put(field.getFieldName().toLowerCase(), field.getFilterType());
column.setUpdateUserId(tokenData.getUserId()); }
onlineColumnService.updateById(column); if (fieldFilterTypeMap.isEmpty()) {
redissonClient.getBucket(OnlineRedisKeyUtil.makeOnlineTableKey(masterTableId)).delete(); return;
}
// 一次 IN 查询取回该表下目标列;历史脏数据可能同表同名列多条,取第一条(与原单条查询语义一致)
List<OnlineColumn> columnList = onlineColumnService.list(new LambdaQueryWrapper<OnlineColumn>().eq(OnlineColumn::getTableId, masterTableId).in(OnlineColumn::getColumnName, fieldFilterTypeMap.keySet()));
if (CollUtil.isEmpty(columnList)) {
return;
}
Map<String, OnlineColumn> columnMap = new HashMap<>();
for (OnlineColumn column : columnList) {
if (column.getColumnName() != null) {
columnMap.putIfAbsent(column.getColumnName().toLowerCase(), column);
}
}
Date now = new Date();
List<OnlineColumn> updateList = new ArrayList<>();
for (Map.Entry<String, Integer> entry : fieldFilterTypeMap.entrySet()) {
OnlineColumn column = columnMap.get(entry.getKey());
if (column == null || Objects.equals(column.getFilterType(), entry.getValue())) {
continue;
} }
column.setFilterType(entry.getValue());
column.setUpdateTime(now);
column.setUpdateUserId(tokenData.getUserId());
updateList.add(column);
}
if (updateList.isEmpty()) {
return;
} }
onlineColumnService.updateBatchById(updateList);
redissonClient.getBucket(OnlineRedisKeyUtil.makeOnlineTableKey(masterTableId)).delete();
} }
// ============ 组件json列id回填 ============ // ============ 组件json列id回填 ============
@ -890,6 +927,9 @@ public class OnlineFormController {
if (CollUtil.isEmpty(fieldList)) { if (CollUtil.isEmpty(fieldList)) {
return; return;
} }
// ① 收集待解析字段与所需关联表单id,避免逐字段三次查询
Map<OnlineQuickFormField, Long> fieldFormIdMap = new LinkedHashMap<>();
Set<Long> relatedFormIdSet = new LinkedHashSet<>();
for (OnlineQuickFormField field : fieldList) { for (OnlineQuickFormField field : fieldList) {
if (field.getFieldType() == null || field.getFieldType() != 402) { if (field.getFieldType() == null || field.getFieldType() != 402) {
continue; continue;
@ -898,21 +938,43 @@ public class OnlineFormController {
if (relatedFormId == null) { if (relatedFormId == null) {
continue; continue;
} }
OnlineForm relatedForm = onlineFormService.getById(relatedFormId); fieldFormIdMap.put(field, relatedFormId);
relatedFormIdSet.add(relatedFormId);
}
if (relatedFormIdSet.isEmpty()) {
return;
}
// ② 批量取关联在线表单
Map<Long, OnlineForm> formMap = onlineFormService.listByIds(relatedFormIdSet).stream().collect(Collectors.toMap(OnlineForm::getFormId, Function.identity(), (a, b) -> a));
Set<Long> relatedTableIdSet = formMap.values().stream().map(OnlineForm::getMasterTableId).filter(Objects::nonNull).collect(Collectors.toSet());
// ③ 批量取关联主表
Map<Long, OnlineTable> tableMap = new HashMap<>();
if (!relatedTableIdSet.isEmpty()) {
onlineTableService.listByIds(relatedTableIdSet).forEach(table -> tableMap.putIfAbsent(table.getTableId(), table));
}
Set<String> relatedTableNameSet = tableMap.values().stream().map(OnlineTable::getTableName).filter(StringUtils::isNotBlank).collect(Collectors.toSet());
// ④ 批量取表头,同表名多条时取第一条
Map<String, OnlFormHead> headMap = new HashMap<>();
if (!relatedTableNameSet.isEmpty()) {
onlFormHeadService.list(new LambdaQueryWrapper<OnlFormHead>().in(OnlFormHead::getTableName, relatedTableNameSet)).forEach(head -> headMap.putIfAbsent(head.getTableName(), head));
}
// ⑤ 内存回填
for (Map.Entry<OnlineQuickFormField, Long> entry : fieldFormIdMap.entrySet()) {
OnlineForm relatedForm = formMap.get(entry.getValue());
if (relatedForm == null || relatedForm.getMasterTableId() == null) { if (relatedForm == null || relatedForm.getMasterTableId() == null) {
continue; continue;
} }
OnlineTable relatedTable = onlineTableService.getById(relatedForm.getMasterTableId()); OnlineTable relatedTable = tableMap.get(relatedForm.getMasterTableId());
if (relatedTable == null) { if (relatedTable == null) {
continue; continue;
} }
OnlFormHead relatedHead = onlFormHeadService.getOne(new LambdaQueryWrapper<OnlFormHead>().eq(OnlFormHead::getTableName, relatedTable.getTableName())); OnlFormHead relatedHead = headMap.get(relatedTable.getTableName());
if (relatedHead == null) { if (relatedHead == null) {
continue; continue;
} }
field.setRefTableId(relatedHead.getId()); entry.getKey().setRefTableId(relatedHead.getId());
field.setRefTableName(relatedTable.getTableName()); entry.getKey().setRefTableName(relatedTable.getTableName());
field.setRefFormCode(relatedForm.getFormCode()); entry.getKey().setRefFormCode(relatedForm.getFormCode());
} }
} }
@ -1060,7 +1122,8 @@ public class OnlineFormController {
// 已有字段:字段控件带 slaveTableId(关联组件)则更新 refPropert;不带(如字典下拉)则清空残留,避免实体表误按关联表强转 Map 报错 // 已有字段:字段控件带 slaveTableId(关联组件)则更新 refPropert;不带(如字典下拉)则清空残留,避免实体表误按关联表强转 Map 报错
boolean isTableColumn = !widget.containsKey("bindData"); boolean isTableColumn = !widget.containsKey("bindData");
Integer comType = widget.getInteger("comType"); Integer comType = widget.getInteger("comType");
boolean isAssociation = !isTableColumn || (comType != null && comType == 402); // 404(关联多选)同为关联组件:保留 refPropert = F7 关联表id,中间表信息由 refMultiTableId 承载
boolean isAssociation = !isTableColumn || (comType != null && (comType == 402 || comType == WidgetFieldTypeMapping.WIDGET_TYPE_MULTI_RELATION));
String slaveTableId = extractSlaveTableId(widget); String slaveTableId = extractSlaveTableId(widget);
if (isAssociation && StrUtil.isNotEmpty(slaveTableId)) { if (isAssociation && StrUtil.isNotEmpty(slaveTableId)) {
if (!Objects.equals(existingField.getRefPropert(), Long.valueOf(slaveTableId))) { if (!Objects.equals(existingField.getRefPropert(), Long.valueOf(slaveTableId))) {
@ -1183,6 +1246,193 @@ public class OnlineFormController {
} }
// ============ 多选关联(404)中间表同步 ============
/**
* 扫描 widgetJson 中的多选关联控件widgetType=404为每个控件创建/复用其专用中间表并把中间表 headId 回写绑定字段
* <p>
* 前置条件控件绑定的字段必须已存在于 onl_form_field锚点列由 syncNewFieldsAndSubTables 负责创建
* 未填关联表名称宿主导表不存在绑定字段不存在时均不处理
*
* @param onlineForm 在线表单以其 widgetJson 为准
* @param datasource 数据源
* @return 是否发生变更
*/
private boolean syncMultiRelationTables(OnlineForm onlineForm, OnlineDatasource datasource) {
if (datasource == null || StrUtil.isBlank(onlineForm.getWidgetJson())) {
return false;
}
List<WidgetJsonUtil.MultiRelationWidget> multiRelationWidgetList = WidgetJsonUtil.collectMultiRelationWidgets(JSON.parseObject(onlineForm.getWidgetJson()));
if (CollUtil.isEmpty(multiRelationWidgetList)) {
return false;
}
boolean changed = false;
for (WidgetJsonUtil.MultiRelationWidget widget : multiRelationWidgetList) {
// 宿主可以是主表,也可以是分录表(404 列落在分录行上),按控件所属表解析
OnlFormHead hostHead = this.resolveHostHead(widget.getTableKey());
if (hostHead == null) {
continue;
}
OnlFormField boundField = onlFormFieldService.getOne(new LambdaQueryWrapper<OnlFormField>().eq(OnlFormField::getHeadId, hostHead.getId()).eq(OnlFormField::getFieldName, widget.getColumnName()));
if (boundField == null) {
// 绑定字段尚未创建(锚点列未同步),不处理
continue;
}
Long relationHeadId = this.createRelationTable(hostHead, boundField, widget.getRelationTableName(), datasource);
if (relationHeadId == null) {
continue;
}
// 只更新关联 id,避免覆盖该字段行其它字段:
// refMultiTableId = 中间表 headId;refPropert = F7 关联目标表 headId(orm 的 MultiLink 靠它反查目标表,缺了会直接校验失败)
Long targetHeadId = this.resolveTargetHeadId(widget);
OnlFormField fieldUpdate = new OnlFormField();
fieldUpdate.setId(boundField.getId());
fieldUpdate.setRefMultiTableId(relationHeadId);
if (targetHeadId != null) {
fieldUpdate.setRefPropert(targetHeadId);
}
boolean refChanged = targetHeadId != null && !Objects.equals(boundField.getRefPropert(), targetHeadId);
if (Objects.equals(relationHeadId, boundField.getRefMultiTableId()) && !refChanged) {
continue;
}
onlFormFieldService.updateById(fieldUpdate);
// 元数据已变:清宿主表缓存,避免 EntityTable 带旧结构存活到缓存过期
this.evictHostTableCache(hostHead);
changed = true;
}
return changed;
}
/**
* 解析 404 控件 F7 目标表的 headId
* 优先用 widget slaveTableId 直接查 402 一致查不到时用 slaveTableName 兜底按表名查
*
* @param widget 404 控件绑定信息
* @return 目标表 onl_form_head.id均查不到返回 null
*/
private Long resolveTargetHeadId(WidgetJsonUtil.MultiRelationWidget widget) {
if (widget.getSlaveTableId() != null) {
OnlFormHead targetHead = onlFormHeadService.getById(widget.getSlaveTableId());
if (targetHead != null) {
return targetHead.getId();
}
}
if (StrUtil.isNotBlank(widget.getSlaveTableName())) {
OnlFormHead targetHead = onlFormHeadService.getOne(new LambdaQueryWrapper<OnlFormHead>().eq(OnlFormHead::getTableName, widget.getSlaveTableName().toLowerCase()));
if (targetHead != null) {
return targetHead.getId();
}
}
return null;
}
/**
* 清宿主表的字段列表与实体元数据缓存中间表 headId 回写后调用
*/
private void evictHostTableCache(OnlFormHead hostHead) {
redissonClient.getBucket(CacheKey.makeTableKey(hostHead.getTableName())).delete();
redissonClient.getBucket(CacheKey.makeChildFieldListKey(hostHead.getId())).delete();
metaCacheService.removeData(hostHead.getTableName());
}
/**
* 解析控件所属表的表头表标识为纯数字时按 zz_online_table.table_id 否则按表名取
*/
private OnlFormHead resolveHostHead(String tableKey) {
if (StrUtil.isBlank(tableKey)) {
return null;
}
OnlineTable hostTable = tableKey.matches("\\d+") ? onlineTableService.getById(Long.valueOf(tableKey)) : onlineTableService.getOne(new LambdaQueryWrapper<OnlineTable>().eq(OnlineTable::getTableName, tableKey.toLowerCase()));
if (hostTable == null) {
return null;
}
return onlFormHeadService.getOne(new LambdaQueryWrapper<OnlFormHead>().eq(OnlFormHead::getTableName, hostTable.getTableName()));
}
/**
* 创建或复用多选关联中间表物理表名 = t_relation_ + 逻辑表名
* 流程幂等判断 onl_form_head onl_form_field syncDB 建物理表 zz_online_table 记录
*
* @param hostHead 宿主表头主表或分录表
* @param boundField 多选控件绑定的字段锚点列
* @param logicTableName 前端填写的关联表名称逻辑名
* @param datasource 数据源
* @return 中间表的 onl_form_head.id创建失败返回 null
*/
private Long createRelationTable(OnlFormHead hostHead, OnlFormField boundField, String logicTableName, OnlineDatasource datasource) {
String relationTableName = "t_relation_" + logicTableName.trim().toLowerCase();
// 幂等:元数据已存在时不再重建(物理表的存在与结构由 syncRelationTable 自行判断)
OnlFormHead existsHead = onlFormHeadService.getOne(new LambdaQueryWrapper<OnlFormHead>().eq(OnlFormHead::getTableName, relationTableName));
if (existsHead != null) {
this.ensureRelationOnlineTable(relationTableName, datasource);
return existsHead.getId();
}
// ② 表头:table_type=3,且不设置 sort(避免被 getOneToManyOnlFormHeadCache 当作普通子表发现)
OnlFormHead relationHead = new OnlFormHead();
relationHead.setId(idGenerator.nextLongId());
relationHead.setTableName(relationTableName);
relationHead.setTableTxt(hostHead.getTableTxt() + "-" + boundField.getFieldRemark() + "多选关联表");
relationHead.setTableType(CustomUtil.MetaType_Relation);
relationHead.setStatus("0");
relationHead.setLine(0);
// ③ 字段行
OnlFormHeadDto relationHeadDto = new OnlFormHeadDto();
BeanUtil.copyProperties(relationHead, relationHeadDto);
relationHeadDto.setOnlFormFieldList(this.buildRelationTableFields());
onlFormHeadService.addAll(relationHeadDto);
// ④ 建物理表(中间表无 id 列、biz_id+obj_id 联合主键,走专用建表并复用同步收尾)
OnlFormHead savedHead = onlFormHeadService.getById(relationHead.getId());
if (savedHead == null) {
return null;
}
onlFormHeadService.syncRelationTable(savedHead);
// ⑤ 建 zz_online_table 记录(须在物理表建好之后,从库反查表结构)
this.ensureRelationOnlineTable(relationTableName, datasource);
return savedHead.getId();
}
/**
* 中间表字段定义id主键biz_id宿主行idobj_id所选记录主键
*/
private List<OnlFormField> buildRelationTableFields() {
List<OnlFormField> relationFieldList = new ArrayList<>(3);
// 中间表无 id 列,biz_id + obj_id 为联合主键(详见 IOnlFormHeadService#syncRelationTable)
relationFieldList.add(this.buildRelationTableField("biz_id", "主表业务ID", 1));
relationFieldList.add(this.buildRelationTableField("obj_id", "关联表ID", 1));
// orm 的 MultiLinkInsertExecutor 会写入 seq(勾选顺序),缺列会导致中间表插入失败
relationFieldList.add(this.buildRelationTableField("seq", "排序顺序", 0));
return relationFieldList;
}
private OnlFormField buildRelationTableField(String fieldName, String fieldRemark, int isKey) {
OnlFormField field = new OnlFormField();
field.setFieldName(fieldName);
field.setFieldRemark(fieldRemark);
field.setFieldType("seq".equals(fieldName) ? CustomUtil.FieldType_int : CustomUtil.FieldType_bigint);
field.setIsKey(isKey);
field.setIsEmpty(isKey == 1 ? 0 : 1);
field.setIsDefault(1);
field.setRowDisabled(1);
field.setSyncFlag(0);
field.setDeletedFlag(GlobalDeletedFlag.NORMAL);
return field;
}
/**
* 兜底创建中间表的 zz_online_table 记录物理表已存在时可从库反查表结构
*/
private void ensureRelationOnlineTable(String relationTableName, OnlineDatasource datasource) {
OnlineTable existsOnlineTable = onlineTableService.getOne(new LambdaQueryWrapper<OnlineTable>().eq(OnlineTable::getTableName, relationTableName.toLowerCase()));
if (existsOnlineTable != null) {
return;
}
OnlineDblink dblink = onlineDblinkService.getById(datasource.getDblinkId());
SqlTable relationSqlTable = onlineDblinkService.getDblinkTable(dblink, relationTableName);
if (relationSqlTable != null) {
onlineTableService.saveNewFromSqlTable(relationSqlTable);
}
}
/** /**
* 提取控件/子表列绑定的从表 idslaveTableId * 提取控件/子表列绑定的从表 idslaveTableId
* 字段控件在 bindData 子表列对象直接挂在顶层 * 字段控件在 bindData 子表列对象直接挂在顶层

39
common/common-online/src/main/java/apelet/common/online/controller/OnlineOperationController.java

@ -37,6 +37,7 @@ import apelet.common.online.model.constant.RelationType;
import apelet.common.online.service.*; import apelet.common.online.service.*;
import apelet.common.online.util.OnlineConstant; import apelet.common.online.util.OnlineConstant;
import apelet.common.online.util.OnlineOperationHelper; import apelet.common.online.util.OnlineOperationHelper;
import apelet.common.online.util.WidgetFieldTypeMapping;
import apelet.common.online.util.WidgetJsonUtil; import apelet.common.online.util.WidgetJsonUtil;
import apelet.common.online.vo.OnlineEventPluginExecuteVo; import apelet.common.online.vo.OnlineEventPluginExecuteVo;
import apelet.common.orm.impl.*; import apelet.common.orm.impl.*;
@ -1033,6 +1034,9 @@ public class OnlineOperationController {
} }
} }
Selector selector = getSelector(selectorSet, masterTable,true); Selector selector = getSelector(selectorSet, masterTable,true);
// 404 多选字段(MultiLinkEntityColumn)的选中集合也以 ObjectCollection 挂在行上,
// 与分录集合区分开后放 rowData 回显,避免被当分录塞进 listData(entryIdMap 里查不到 key 会存成 "null")。
Set<String> multiLinkFieldSet = ormGenDataSourceUtil.getEntityTable(masterTable.getTableName()).getEntityColumnList().stream().filter(c -> c instanceof MultiLinkEntityColumn).map(c -> c.getColumnName().toLowerCase()).collect(Collectors.toSet());
// 附表排序(SQL 层):配置了 sortField 的分录表按 分录表名.字段 排序。 // 附表排序(SQL 层):配置了 sortField 的分录表按 分录表名.字段 排序。
Sorter sorter = null; Sorter sorter = null;
if (!entrySortFieldMap.isEmpty()) { if (!entrySortFieldMap.isEmpty()) {
@ -1064,10 +1068,18 @@ public class OnlineOperationController {
Map<String, Object> map = resultList.get(0); Map<String, Object> map = resultList.get(0);
map.keySet().forEach(k -> { map.keySet().forEach(k -> {
if (map.get(k) instanceof ObjectCollection) { if (map.get(k) instanceof ObjectCollection && multiLinkFieldSet.contains(k)) {
// 404 多选回显:选中记录集合转 List 放表头字段(rowData),前端按记录回填多选组件。
List<Map<String, Object>> multiLinkList = new ArrayList<>();
onlineOperationService.collectionToJson((ObjectCollection) map.get(k), multiLinkList);
rowData.put(k, multiLinkList);
} else if (map.get(k) instanceof ObjectCollection) {
ObjectCollection entryCollection = (ObjectCollection) map.get(k); ObjectCollection entryCollection = (ObjectCollection) map.get(k);
List<Map<String, Object>> entryList = new ArrayList<>(); List<Map<String, Object>> entryList = new ArrayList<>();
onlineOperationService.collectionToJson(entryCollection, entryList); onlineOperationService.collectionToJson(entryCollection, entryList);
// 分录行内的 404 多选集合同样要展开:collectionToJson 只展开嵌套 ObjectValue(F7),
// ObjectCollection 会被原样塞进 JSON(bean 形状),前端拿不到选中记录数组。
this.expandEntryMultiLinkFields(k, entryList);
// 附表配置了 sortField 时已在 SQL 层排序,这里不再按 seq 内存排序。 // 附表配置了 sortField 时已在 SQL 层排序,这里不再按 seq 内存排序。
if (!entrySortFieldMap.containsKey(k)) { if (!entrySortFieldMap.containsKey(k)) {
String sort = "seq"; String sort = "seq";
@ -1136,6 +1148,13 @@ public class OnlineOperationController {
OnlineColumn onlineColumn = columnMap.get(columnId); OnlineColumn onlineColumn = columnMap.get(columnId);
if (onlineColumn == null) return; if (onlineColumn == null) return;
if (data.containsKey("slaveTableId")) { if (data.containsKey("slaveTableId")) {
// 关联多选(404):集合以绑定字段名为 key 挂在宿主对象上,selector 只需裸列名(jar 据此填充中间表数据),
// 不走 F7 的 「.id / .displayField」拼法
Integer widgetType = jsonObject.getInteger("widgetType");
if (widgetType != null && widgetType == WidgetFieldTypeMapping.WIDGET_TYPE_MULTI_RELATION) {
selectorSet.add(onlineColumn.getColumnName().toLowerCase());
return;
}
// F7 字段 // F7 字段
String string = jsonObject.getJSONObject("props").getJSONObject("relativeTable").getString("displayField"); String string = jsonObject.getJSONObject("props").getJSONObject("relativeTable").getString("displayField");
selectorSet.add(onlineColumn.getColumnName().toLowerCase() + "." + "id"); selectorSet.add(onlineColumn.getColumnName().toLowerCase() + "." + "id");
@ -1154,6 +1173,24 @@ public class OnlineOperationController {
} }
} }
/**
* 展开分录行内的 404 多选字段按分录表 EntityTable MultiLinkEntityColumn 列名定位
* 把选中记录集合转成 List&lt;Map&gt;供前端按选中记录回填多选组件
*/
private void expandEntryMultiLinkFields(String entryTableName, List<Map<String, Object>> entryList) {
Set<String> multiLinkFields = ormGenDataSourceUtil.getEntityTable(entryTableName).getEntityColumnList().stream().filter(c -> c instanceof MultiLinkEntityColumn).map(c -> c.getColumnName().toLowerCase()).collect(Collectors.toSet());
if (multiLinkFields.isEmpty()) return;
for (Map<String, Object> row : entryList) {
multiLinkFields.forEach(field -> {
if (row.get(field) instanceof ObjectCollection) {
List<Map<String, Object>> multiLinkList = new ArrayList<>();
onlineOperationService.collectionToJson((ObjectCollection) row.get(field), multiLinkList);
row.put(field, multiLinkList);
}
});
}
}
/** /**
* 解析过滤表达式中的 ${字段名} 占位符取值转成带引号的字符串满足 FilterParser 值只接受字符串/null * 解析过滤表达式中的 ${字段名} 占位符取值转成带引号的字符串满足 FilterParser 值只接受字符串/null

3
common/common-online/src/main/java/apelet/common/online/service/impl/OnlineDatasourceServiceImpl.java

@ -251,8 +251,7 @@ public class OnlineDatasourceServiceImpl extends BaseService<OnlineDatasource, L
@Override @Override
public OnlineDatasource getOnlineDatasourceByMasterTableId(Long masterTableId) { public OnlineDatasource getOnlineDatasourceByMasterTableId(Long masterTableId) {
return onlineDatasourceMapper.selectOne( return onlineDatasourceMapper.selectOne(new LambdaQueryWrapper<OnlineDatasource>().eq(OnlineDatasource::getMasterTableId, masterTableId), false);
new LambdaQueryWrapper<OnlineDatasource>().eq(OnlineDatasource::getMasterTableId, masterTableId));
} }
@Override @Override

65
common/common-online/src/main/java/apelet/common/online/service/impl/OnlineFormServiceImpl.java

@ -26,6 +26,7 @@ import apelet.common.online.util.OnlineRedisKeyUtil;
import apelet.common.orm.impl.EntityColumn; import apelet.common.orm.impl.EntityColumn;
import apelet.common.orm.impl.EntityTable; import apelet.common.orm.impl.EntityTable;
import apelet.common.orm.impl.LinkEntityColumn; import apelet.common.orm.impl.LinkEntityColumn;
import apelet.common.orm.impl.MultiLinkEntityColumn;
import apelet.common.redis.util.CommonRedisUtil; import apelet.common.redis.util.CommonRedisUtil;
import apelet.common.sequence.wrapper.IdGeneratorWrapper; import apelet.common.sequence.wrapper.IdGeneratorWrapper;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
@ -536,7 +537,26 @@ public class OnlineFormServiceImpl extends BaseService<OnlineForm, Long> impleme
public void setEntry(ObjectValue objectValue, @NotNull EntityTable entityTable, Map<String, Object> tableData) { public void setEntry(ObjectValue objectValue, @NotNull EntityTable entityTable, Map<String, Object> tableData) {
for (EntityColumn entityColumn : entityTable.getEntityColumnList()) { for (EntityColumn entityColumn : entityTable.getEntityColumnList()) {
String field_name = entityColumn.getColumnName(); String field_name = entityColumn.getColumnName();
if (entityColumn instanceof LinkEntityColumn) { if (entityColumn instanceof MultiLinkEntityColumn) {
// 404 多选关联:集合语义,值必须是 ObjectCollection(元素 = 所选记录),key 为绑定字段名,由 orm 落中间表
Object obj = tableData.get(field_name);
if (obj == null || obj.toString().equals("")) {
continue;
}
ObjectCollection multiLinkCollection = new ObjectCollection();
for (Object element : this.toElementList(obj)) {
ObjectValue elementValue = new ObjectValue(field_name);
// orm 的 ObjectValue#getPkValue 只认 String/Long(其它 Number 会 (Long) 强转抛 CCE),
// 前端可能传数字或字符串,统一转成 Long 再放进去
Long elementId = this.toLongValue(element instanceof Map ? ((Map) element).get("id") : element);
if (elementId == null) {
continue;
}
elementValue.put("id", elementId);
multiLinkCollection.addObject(elementValue);
}
objectValue.put(field_name, multiLinkCollection);
} else if (entityColumn instanceof LinkEntityColumn) {
String tableName = ((LinkEntityColumn) entityColumn).getEntityLinkRelation().getSuperTable().getTableName(); String tableName = ((LinkEntityColumn) entityColumn).getEntityLinkRelation().getSuperTable().getTableName();
Object obj = tableData.get(field_name); Object obj = tableData.get(field_name);
if (obj == null || obj.toString().equals("")) continue; if (obj == null || obj.toString().equals("")) continue;
@ -561,4 +581,47 @@ public class OnlineFormServiceImpl extends BaseService<OnlineForm, Long> impleme
} }
} }
/**
* 把多选关联控件的入参值统一成可遍历的元素集合
* 前端可能传 JSON 数组正常或单个对象/标量单选误配此处统一兼容
*
* @param obj 入参原始值
* @return 元素列表
*/
private List<Object> toElementList(Object obj) {
if (obj instanceof Collection) {
return new ArrayList<>((Collection) obj);
}
List<Object> elementList = new ArrayList<>(1);
elementList.add(obj);
return elementList;
}
/**
* 把多选元素的主键值统一转成 Long兼容前端传数字Integer/Long/BigDecimal或字符串的情况
*
* @param value 入参主键值
* @return 转换后的 Long无法转换返回 null调用方跳过该元素
*/
private Long toLongValue(Object value) {
if (value == null) {
return null;
}
if (value instanceof Long) {
return (Long) value;
}
if (value instanceof Number) {
return ((Number) value).longValue();
}
String text = value.toString().trim();
if (text.isEmpty() || "null".equals(text)) {
return null;
}
try {
return Long.valueOf(text);
} catch (NumberFormatException e) {
return null;
}
}
} }

10
common/common-online/src/main/java/apelet/common/online/util/OrmOnlineDataSourceUtil.java

@ -3,8 +3,14 @@ package apelet.common.online.util;
import apelet.common.generator.utils.OrmDataSourceNewUtil; import apelet.common.generator.utils.OrmDataSourceNewUtil;
import apelet.common.online.dao.OnlineDatasourceTableMapper; import apelet.common.online.dao.OnlineDatasourceTableMapper;
import apelet.common.online.model.*; import apelet.common.online.model.*;
import apelet.common.online.service.*; import apelet.common.online.service.OnlineColumnService;
import apelet.common.orm.impl.*; import apelet.common.online.service.OnlineDatasourceRelationService;
import apelet.common.online.service.OnlineDatasourceService;
import apelet.common.online.service.OnlineTableService;
import apelet.common.orm.impl.EntityColumn;
import apelet.common.orm.impl.EntityLinkRelation;
import apelet.common.orm.impl.EntityTable;
import apelet.common.orm.impl.LinkEntityColumn;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;

11
common/common-online/src/main/java/apelet/common/online/util/WidgetFieldTypeMapping.java

@ -5,6 +5,15 @@ import java.util.Map;
public class WidgetFieldTypeMapping { public class WidgetFieldTypeMapping {
/** 多选关联控件类型值。 */
public static final int WIDGET_TYPE_MULTI_RELATION = 404;
/** 单选关联(F7)控件类型值。 */
public static final int WIDGET_TYPE_LINK = 402;
/** 多选关联控件「关联表名称」字段名(中间表逻辑名,前端设计器填写;主表单在 props 下,分录列在顶层)。 */
public static final String MULTI_RELATION_TABLE_NAME_KEY = "relationTableName";
private WidgetFieldTypeMapping() { private WidgetFieldTypeMapping() {
} }
@ -64,6 +73,8 @@ public class WidgetFieldTypeMapping {
WIDGET_TO_FIELD_TYPE.put(401, "2"); WIDGET_TO_FIELD_TYPE.put(401, "2");
// 关联选择 → bigint // 关联选择 → bigint
WIDGET_TO_FIELD_TYPE.put(402, "2"); WIDGET_TO_FIELD_TYPE.put(402, "2");
// 关联多选 → bigint(绑定字段仅作锚点列,不存多选值,多选集合存专用中间表)
WIDGET_TO_FIELD_TYPE.put(404, "2");
// 时间选择器 → datetime // 时间选择器 → datetime
WIDGET_TO_FIELD_TYPE.put(652, "3"); WIDGET_TO_FIELD_TYPE.put(652, "3");
// 文件上传 → varchar // 文件上传 → varchar

217
common/common-online/src/main/java/apelet/common/online/util/WidgetJsonUtil.java

@ -8,13 +8,9 @@ import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import jodd.util.StringUtil; import jodd.util.StringUtil;
import lombok.Getter;
import java.util.ArrayList; import java.util.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
@ -394,6 +390,194 @@ public class WidgetJsonUtil {
} }
/** /**
* 收集 widgetJson 中多选关联控件widgetType=404的绑定信息
* 仅收集已填写关联表名称的控件字段名与所属表标识的取值口径与 {@link #buildTableFieldMap(JSONObject)} 保持一致
*
* @param widgetJson 在线表单的 widgetJson
* @return 多选关联控件绑定信息列表无则为空列表
*/
public static List<MultiRelationWidget> collectMultiRelationWidgets(JSONObject widgetJson) {
List<MultiRelationWidget> multiRelationWidgetList = new ArrayList<>();
if (widgetJson == null) {
return multiRelationWidgetList;
}
for (Map.Entry<String, Map<String, JSONObject>> tableEntry : buildTableFieldMap(widgetJson).entrySet()) {
for (Map.Entry<String, JSONObject> fieldEntry : tableEntry.getValue().entrySet()) {
JSONObject widget = fieldEntry.getValue();
// 两种形状:主表单字段控件(widgetType=404)、分录表格列(comType=404,列对象没有 widgetType)
boolean isLeafWidget = isMultiRelationWidget(widget);
boolean isTableColumn = widget.getInteger("widgetType") == null && isMultiRelationColumn(widget);
if (!isLeafWidget && !isTableColumn) {
continue;
}
String relationTableName = resolveRelationTableName(widget);
if (StrUtil.isEmpty(relationTableName)) {
continue;
}
Long[] refIds = resolveMultiRelationRefIds(widget);
multiRelationWidgetList.add(new MultiRelationWidget(tableEntry.getKey(), fieldEntry.getKey(), relationTableName.trim(), refIds[0], resolveMultiRelationTargetTableName(widget), refIds[1]));
}
}
return multiRelationWidgetList;
}
/**
* 多选关联控件主表单字段控件widgetType = 404
*/
public static boolean isMultiRelationWidget(JSONObject widget) {
if (widget == null) {
return false;
}
Integer widgetType = widget.getInteger("widgetType");
return widgetType != null && widgetType == WidgetFieldTypeMapping.WIDGET_TYPE_MULTI_RELATION;
}
/**
* 是否为分录表格里的多选关联列列对象没有 widgetType控件类型由 comType 承载
*/
public static boolean isMultiRelationColumn(JSONObject widget) {
if (widget == null) {
return false;
}
Integer comType = widget.getInteger("comType");
return comType != null && comType == WidgetFieldTypeMapping.WIDGET_TYPE_MULTI_RELATION;
}
/**
* 是否为分录表格里的关联列F7 单选 402 关联多选 404列对象没有 widgetType控件类型由 comType 承载
*/
public static boolean isAssociationColumn(JSONObject widget) {
if (widget == null) {
return false;
}
Integer comType = widget.getInteger("comType");
return comType != null && (comType == WidgetFieldTypeMapping.WIDGET_TYPE_LINK || comType == WidgetFieldTypeMapping.WIDGET_TYPE_MULTI_RELATION);
}
/**
* 读取多选关联控件中间表元数据所需的两个关联 idF7 关联表目标表headId 与所属表 tableId
* 取值位置按控件形状区分
* <ul>
* <li>主表单字段控件bindData.slaveTableId目标表bindData.tableId所属表</li>
* <li>分录表格列 bindData顶层 slaveTableId / tableId</li>
* </ul>
*
* @param widget 404 控件或其分录列对象
* @return 长度 2 的数组 [目标表headId, 所属表tableId]缺失项为 null
*/
public static Long[] resolveMultiRelationRefIds(JSONObject widget) {
if (widget == null) {
return new Long[]{null, null};
}
JSONObject bindData = widget.getJSONObject("bindData");
boolean isTableColumn = !widget.containsKey("bindData");
String slaveTableId = isTableColumn ? widget.getString("slaveTableId") : (bindData == null ? null : bindData.getString("slaveTableId"));
String tableId = isTableColumn ? widget.getString("tableId") : (bindData == null ? null : bindData.getString("tableId"));
return new Long[]{toLongOrNull(slaveTableId), toLongOrNull(tableId)};
}
/**
* 读取多选关联控件的 F7 目标表名bindData.slaveTableName / 列顶层 slaveTableName
* 用于在 slaveTableId 解析不到 onl_form_head 时兜底定位目标表
*
* @param widget 404 控件或其分录列对象
* @return 目标表物理名未配置返回 null
*/
public static String resolveMultiRelationTargetTableName(JSONObject widget) {
if (widget == null) {
return null;
}
JSONObject bindData = widget.getJSONObject("bindData");
String slaveTableName = widget.containsKey("bindData")
? (bindData == null ? null : bindData.getString("slaveTableName"))
: widget.getString("slaveTableName");
return StrUtil.isBlank(slaveTableName) ? null : slaveTableName.trim();
}
/**
* 字符串转 Long非数字如尚未落库的新建列标识返回 null
*/
private static Long toLongOrNull(String value) {
if (StrUtil.isBlank(value) || !value.trim().matches("\\d+")) {
return null;
}
try {
return Long.valueOf(value.trim());
} catch (NumberFormatException e) {
return null;
}
}
/**
* 读取多选关联控件配置的关联表名称中间表逻辑名
* 取值位置按优先级props前端设计器实际写入位置 bindData 控件顶层分录列使用
*
* @param widget 404 控件或其分录列对象
* @return 关联表名称未配置返回 null
*/
public static String resolveRelationTableName(JSONObject widget) {
if (widget == null) {
return null;
}
JSONObject props = widget.getJSONObject("props");
if (props != null && StrUtil.isNotEmpty(props.getString(WidgetFieldTypeMapping.MULTI_RELATION_TABLE_NAME_KEY))) {
return props.getString(WidgetFieldTypeMapping.MULTI_RELATION_TABLE_NAME_KEY);
}
JSONObject bindData = widget.getJSONObject("bindData");
if (bindData != null && StrUtil.isNotEmpty(bindData.getString(WidgetFieldTypeMapping.MULTI_RELATION_TABLE_NAME_KEY))) {
return bindData.getString(WidgetFieldTypeMapping.MULTI_RELATION_TABLE_NAME_KEY);
}
return widget.getString(WidgetFieldTypeMapping.MULTI_RELATION_TABLE_NAME_KEY);
}
/**
* 多选关联控件widgetType=404的绑定信息
*/
@Getter
public static class MultiRelationWidget {
/**
* 绑定字段所属表标识bindData.tableId缺省 tableName
*/
private final String tableKey;
/**
* 绑定字段名bindData.columnName缺省 columnFieldName
*/
private final String columnName;
/**
* 关联表名称props.relationTableName即中间表逻辑名
*/
private final String relationTableName;
/**
* F7 关联目标表 headIdbindData.slaveTableId / 列顶层 slaveTableId用于回写 onl_form_field.refPropert
*/
private final Long slaveTableId;
/**
* F7 关联目标表物理名bindData.slaveTableNameslaveTableId 取不到目标表时的兜底定位依据
*/
private final String slaveTableName;
/**
* 所属表 tableIdbindData.tableId / 列顶层 tableId用于回写 onl_form_field.headId
*/
private final Long tableId;
public MultiRelationWidget(String tableKey, String columnName, String relationTableName, Long slaveTableId, String slaveTableName, Long tableId) {
this.tableKey = tableKey;
this.columnName = columnName;
this.relationTableName = relationTableName;
this.slaveTableId = slaveTableId;
this.slaveTableName = slaveTableName;
this.tableId = tableId;
}
}
/**
* 收集 pc/mobile 控件树中 列名(columnFieldName) 控件 variableName 列表 * 收集 pc/mobile 控件树中 列名(columnFieldName) 控件 variableName 列表
* 仅收集 pc/mobile widgetList叶子字段控件递归 childWidgetList 下钻 buildTableFieldMap 取值一致 * 仅收集 pc/mobile widgetList叶子字段控件递归 childWidgetList 下钻 buildTableFieldMap 取值一致
* 一个列名可对应多个控件多个控件绑定同一列时返回多个 variableName * 一个列名可对应多个控件多个控件绑定同一列时返回多个 variableName
@ -496,23 +680,32 @@ public class WidgetJsonUtil {
} }
for (int i = 0; i < tableColumnList.size(); i++) { for (int i = 0; i < tableColumnList.size(); i++) {
JSONObject tableColumn = tableColumnList.getJSONObject(i); JSONObject tableColumn = tableColumnList.getJSONObject(i);
if (tableColumn.containsKey("f7ShowName") && StringUtil.isNotBlank(tableColumn.getString("f7ShowName"))) { String columnIdStr = tableColumn.getString("columnId");
// 新增列:字段名存在 columnId 里(非数字),描述在 showName
boolean isNewColumn = StrUtil.isNotEmpty(columnIdStr) && !columnIdStr.matches("\\d+");
// 派生显示列(给已有列 A 配了 F7 显示字段 B,showFieldName = "A.B")只在已有列(columnId 为数字)时出现;
// 新建的关联列(402/404)虽同样带 f7ShowName,但它是锚点列,必须收集,否则字段建不出来。
if (!isNewColumn && !isAssociationColumn(tableColumn) && tableColumn.containsKey("f7ShowName") && StringUtil.isNotBlank(tableColumn.getString("f7ShowName"))) {
//f7 不做字段的搜集 //f7 不做字段的搜集
continue; continue;
} }
Integer fieldType = tableColumn.getInteger("fieldType"); Integer fieldType = tableColumn.getInteger("fieldType");
if (fieldType != null && fieldType == 1) continue; if (fieldType != null && fieldType == 1){ continue;}
String columnIdStr = tableColumn.getString("columnId");
String columnName; String columnName;
if (StrUtil.isNotEmpty(columnIdStr) && !columnIdStr.matches("\\d+")) { if (isNewColumn) {
// 新增列:字段名存在 columnId 里(非数字),描述在 showName
columnName = columnIdStr; columnName = columnIdStr;
} else { } else {
// 已有列:字段名在 showFieldName,兜底 showName // 已有列:字段名在 showFieldName,兜底 showName
columnName = tableColumn.getString("showFieldName"); columnName = tableColumn.getString("showFieldName");
if (StrUtil.isEmpty(columnName)) columnName = tableColumn.getString("showName"); if (StrUtil.isEmpty(columnName)) columnName = tableColumn.getString("showName");
// 关联列(402/404)带显示字段时 showFieldName 是 "<字段名>.<显示字段>" 派生显示名,锚点字段名取点号前部分,
// 否则整串当字段名会查不到已有字段、被当新列执行带点号的 ALTER TABLE
if (isAssociationColumn(tableColumn) && columnName != null && columnName.contains(".")) {
columnName = columnName.substring(0, columnName.indexOf('.'));
}
} }
if (StrUtil.isEmpty(columnName)) continue; // 派生显示列若仍带点号说明不是真实字段,跳过(兜底防护)
if (StrUtil.isEmpty(columnName) || columnName.contains(".")){ continue;}
columnMap.put(columnName, tableColumn); columnMap.put(columnName, tableColumn);
} }
} }

Loading…
Cancel
Save