diff --git a/common/common-flow/src/main/java/apelet/common/flow/listener/AutoSkipTaskListener.java b/common/common-flow/src/main/java/apelet/common/flow/listener/AutoSkipTaskListener.java index 62f7043..5a7ae0e 100644 --- a/common/common-flow/src/main/java/apelet/common/flow/listener/AutoSkipTaskListener.java +++ b/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 org.flowable.bpmn.model.ExtensionAttribute; import org.flowable.bpmn.model.UserTask; +import org.flowable.common.engine.api.delegate.Expression; 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.service.delegate.DelegateTask; @@ -56,7 +60,7 @@ public class AutoSkipTaskListener implements TaskListener { UserTask userTask = flowApiService.getUserTask(t.getProcessDefinitionId(), t.getTaskDefinitionKey()); List attributes = userTask.getAttributes().get(FlowConstant.USER_TASK_AUTO_SKIP_KEY); Set 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)) { return; } @@ -108,22 +112,29 @@ public class AutoSkipTaskListener implements TaskListener { comment.fillWith(t); comment.setApprovalType(FlowApprovalType.AGREE); comment.setTaskComment(StrFormatter.format("自动跳过审批。审批人 [{}], 跳过原因 [{}]。", - userTask.getAssignee(), this.getMessageBySkipType(skipType))); + assignedUser, this.getMessageBySkipType(skipType))); flowApiService.completeTask(t, comment, null, null); } return comment != null; } - private String getAssignedUser(UserTask userTask, String processDefinitionId, String executionId) { - String assignedUser = userTask.getAssignee(); + private String getAssignedUser(UserTask userTask, DelegateTask t) { + // 引擎在触发create监听器之前,已对assignee配置的表达式完成求值(含${bean.method(execution)}形式的EL表达式),这里直接取任务上的实际处理人。 + String assignedUser = t.getAssignee(); if (StrUtil.isNotBlank(assignedUser)) { - if (assignedUser.startsWith("${") && assignedUser.endsWith("}")) { - String variableName = assignedUser.substring(2, assignedUser.length() - 1); - assignedUser = flowApiService.getExecutionVariableStringWithSafe(executionId, variableName); + return assignedUser; + } + 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 { FlowTaskExt flowTaskExt = flowTaskExtService - .getByProcessDefinitionIdAndTaskId(processDefinitionId, userTask.getId()); + .getByProcessDefinitionIdAndTaskId(t.getProcessDefinitionId(), userTask.getId()); List candidateUsernames; if (StrUtil.isBlank(flowTaskExt.getCandidateUsernames())) { candidateUsernames = Collections.emptyList(); @@ -131,7 +142,7 @@ public class AutoSkipTaskListener implements TaskListener { candidateUsernames = StrUtil.split(flowTaskExt.getCandidateUsernames(), ","); } else { 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, ","); } if (candidateUsernames != null && candidateUsernames.size() == 1) { @@ -141,6 +152,18 @@ public class AutoSkipTaskListener implements TaskListener { 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) { switch (skipType) { case EQ_PREV_SUBMIT_USER: diff --git a/common/common-generator/pom.xml b/common/common-generator/pom.xml index 008dab7..c007714 100644 --- a/common/common-generator/pom.xml +++ b/common/common-generator/pom.xml @@ -59,7 +59,7 @@ apelet common-orm - 1.0.3 + 1.0.4 compile diff --git a/common/common-generator/src/main/java/apelet/common/generator/model/OnlFormField.java b/common/common-generator/src/main/java/apelet/common/generator/model/OnlFormField.java index e5f1b5a..7ecea79 100644 --- a/common/common-generator/src/main/java/apelet/common/generator/model/OnlFormField.java +++ b/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 字段标识") private String easFieldMark; + /** + * 多选关联中间表头id(404 多选关联字段专用,指向中间表的 onl_form_head.id;为空表示非多选字段) + */ + @Schema(description = "多选关联中间表头id") + private Long refMultiTableId; + @@ -100,6 +106,18 @@ public class OnlFormField extends BaseModel implements Serializable { @TableField(exist = false) private Map refPropertMap; + /** + * 多选关联中间表的描述信息,结构与 refPropertMap 一致(id = 中间表 onl_form_head.id,table_name = 中间表物理名),供表单元数据详情回显。 + */ + @RelationDict( + masterIdField = "refMultiTableId", + equalOneToOneRelationField = "onlFormHead", + slaveModelClass = OnlFormHead.class, + slaveIdField = "id", + slaveNameField = "tableTxt") + @TableField(exist = false) + private Map refMultiTableMap; + @RelationGlobalDict( masterIdField = "gradeId", dictCode = "onl_fieldType") diff --git a/common/common-generator/src/main/java/apelet/common/generator/service/IOnlFormFieldService.java b/common/common-generator/src/main/java/apelet/common/generator/service/IOnlFormFieldService.java index fba5e9a..eac3fe9 100644 --- a/common/common-generator/src/main/java/apelet/common/generator/service/IOnlFormFieldService.java +++ b/common/common-generator/src/main/java/apelet/common/generator/service/IOnlFormFieldService.java @@ -25,6 +25,13 @@ public interface IOnlFormFieldService extends IService { */ List selectFormFieldSlaveByHead(Map map); + /** + * 查询表字段和字段关联的多选中间表名(404 多选关联字段专用,按 ref_multi_table_id 关联)。 + * @param map + * @return + */ + List selectMultiTableSlaveByHead(Map map); + List queryOnlFormField(LambdaQueryWrapper lambdaQueryWrapper); List getFiledByTableId(String key); diff --git a/common/common-generator/src/main/java/apelet/common/generator/service/IOnlFormHeadService.java b/common/common-generator/src/main/java/apelet/common/generator/service/IOnlFormHeadService.java index 0172fe3..2672958 100644 --- a/common/common-generator/src/main/java/apelet/common/generator/service/IOnlFormHeadService.java +++ b/common/common-generator/src/main/java/apelet/common/generator/service/IOnlFormHeadService.java @@ -130,4 +130,14 @@ public interface IOnlFormHeadService extends IService { * @return OnlFormHead */ OnlFormHead createMetaAndSync(String tableName, String tableTxt, String metaType, String sortFiled); + + /** + * 创建多选关联中间表并同步数据库。 + *

中间表结构固定为 biz_id/obj_id 联合主键 + seq(无 id 列),由 orm 的 MultiLink 链路按 biz_id/obj_id/seq 三列读写, + * 与通用 {@link #syncDB} 的建表模板(硬编码 id 主键)不兼容,故单独建表后复用同步收尾逻辑。

+ * + * @param onlFormHead 已落库的中间表表头(需含 id/tableName/tableTxt)。 + * @return 同步成功返回 true。 + */ + Boolean syncRelationTable(OnlFormHead onlFormHead); } diff --git a/common/common-generator/src/main/java/apelet/common/generator/service/impl/OnlFormFieldServiceImpl.java b/common/common-generator/src/main/java/apelet/common/generator/service/impl/OnlFormFieldServiceImpl.java index 0f9a02c..be27d6a 100644 --- a/common/common-generator/src/main/java/apelet/common/generator/service/impl/OnlFormFieldServiceImpl.java +++ b/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.generator.dao.OnlFormFieldMapper; import apelet.common.generator.model.OnlFormField; -import apelet.common.generator.model.OnlFormHead; import apelet.common.generator.service.IOnlFormFieldService; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; @@ -14,7 +13,10 @@ import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; 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; /** @@ -49,27 +51,63 @@ public class OnlFormFieldServiceImpl extends ServiceImpl selectMultiTableSlaveByHead(Map 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 queryOnlFormField(LambdaQueryWrapper lambdaQueryWrapper) { List onlFormFieldList = this.list(lambdaQueryWrapper); + if (onlFormFieldList == null || onlFormFieldList.isEmpty()) { + return onlFormFieldList; + } Map map = new HashMap(); - map.put("headId",onlFormFieldList.get(0).getHeadId()); - List list = this.selectFormFieldSlaveByHead(map); - for(OnlFormField onlFormField:onlFormFieldList){ - if(onlFormField.getRefPropert()!=null){ - for(JSONObject jsonObject:list){ - if(jsonObject.getLong("ref_propert").equals(onlFormField.getRefPropert())){ - - Map map1 = new HashMap<>(); - map1.put("table_name", jsonObject.get("table_name")); - map1.put("id",jsonObject.getLong("ref_propert")); - onlFormField.setRefPropertMap(map1); - } - } + map.put("headId", onlFormFieldList.get(0).getHeadId()); + // 两种关联语义各查一次:402 关联表 / 404 多选中间表 + Map refPropertTableNameMap = this.toTableNameMap(this.selectFormFieldSlaveByHead(map), "ref_propert"); + Map refMultiTableNameMap = this.toTableNameMap(this.selectMultiTableSlaveByHead(map), "ref_multi_table_id"); + for (OnlFormField onlFormField : onlFormFieldList) { + // 402 单选关联:关联表描述 + if (onlFormField.getRefPropert() != null && refPropertTableNameMap.containsKey(onlFormField.getRefPropert())) { + Map refPropertMap = new HashMap<>(); + refPropertMap.put("table_name", refPropertTableNameMap.get(onlFormField.getRefPropert())); + refPropertMap.put("id", onlFormField.getRefPropert()); + onlFormField.setRefPropertMap(refPropertMap); + } + // 404 多选关联:中间表描述(结构与 refPropertMap 一致,id = 中间表 onl_form_head.id) + if (onlFormField.getRefMultiTableId() != null && refMultiTableNameMap.containsKey(onlFormField.getRefMultiTableId())) { + Map refMultiTableMap = new HashMap<>(); + refMultiTableMap.put("table_name", refMultiTableNameMap.get(onlFormField.getRefMultiTableId())); + refMultiTableMap.put("id", onlFormField.getRefMultiTableId()); + onlFormField.setRefMultiTableMap(refMultiTableMap); } } return onlFormFieldList; } + /** + * 关联表头id → 表名 的映射,避免嵌套遍历。 + * + * @param list selectFormFieldSlaveByHead / selectMultiTableSlaveByHead 的查询结果。 + * @param refColumnName 关联字段所在列名(ref_propert 或 ref_multi_table_id)。 + * @return 关联表头id → 表名。 + */ + private Map toTableNameMap(List list, String refColumnName) { + Map 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 public List getFiledByTableId(String key) { String sql = "SELECT ff.* FROM onl_form_head fh " + diff --git a/common/common-generator/src/main/java/apelet/common/generator/service/impl/OnlFormHeadServiceImpl.java b/common/common-generator/src/main/java/apelet/common/generator/service/impl/OnlFormHeadServiceImpl.java index 89d50d9..3452439 100644 --- a/common/common-generator/src/main/java/apelet/common/generator/service/impl/OnlFormHeadServiceImpl.java +++ b/common/common-generator/src/main/java/apelet/common/generator/service/impl/OnlFormHeadServiceImpl.java @@ -74,6 +74,16 @@ public class OnlFormHeadServiceImpl extends ServiceImpl RELATION_TABLE_COLUMNS = new HashSet<>(Arrays.asList("biz_id", "obj_id", "seq")); + public static Map typeMap = new HashMap<>(); static { @@ -297,6 +307,16 @@ public class OnlFormHeadServiceImpl extends ServiceImpl() @@ -312,6 +332,49 @@ public class OnlFormHeadServiceImpl extends ServiceImpl 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()); + } + + /** + * 获取实体元数据并补全各类关联的 superTable。 + *

元数据来自 {@link MetaCacheService}(按表名缓存、对象共享可变),因此此处补挂的 superTable 会留在缓存对象上, + * 后续调用可复用。orm 内部会直接取 LinkEntityColumn / MultiLinkEntityColumn 的 superTable 使用, + * 若为 null 会在 ImplUtils 解析结果集时抛 NPE。

+ * + * @param datasourceVariableName 表物理名。 + * @param visiting 当前递归链上已进入的表,用于防环。 + */ + private EntityTable getEntityTable(String datasourceVariableName, Set visiting) { try { - EntityTable entityTable = getEntityTable(true,datasourceVariableName,0); - return entityTable; + return getEntityTable(false, datasourceVariableName, 0, visiting); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); } } + /** + * 多选关联中间表(MultiLinkEntityColumn)的最大解析深度,防止元数据配错导致递归失控。 + */ + private static final int MULTI_LINK_MAX_LEVEL = 3; - - private EntityTable getEntityTable(boolean ismain,String datasourceVariableName, int level) throws IOException, ClassNotFoundException { + private EntityTable getEntityTable(boolean ismain, String datasourceVariableName, int level, Set visiting) throws IOException, ClassNotFoundException { + // 防环:同一递归链上重复进入同一张表说明元数据成环,直接跳过补全 + if (!visiting.add(datasourceVariableName)) { + return (EntityTable) metaCacheService.getData(datasourceVariableName); + } EntityTable entityTable = (EntityTable) metaCacheService.getData(datasourceVariableName); List entityColumnList = entityTable.getEntityColumnList(); 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) { LinkEntityColumn linkEntityColumn = (LinkEntityColumn) entityColumn; if (linkEntityColumn.getEntityLinkRelation().getRelationType() == 1) {//一对多 if (level < 2) { - if (ismain) { - linkEntityColumn.getEntityLinkRelation().setSuperTable(getEntityTable(false, linkEntityColumn.getEntityLinkRelation().getRelationName(), 0)); - } + // 主表这一层保持原有行为(level 归 0);非主表(分录表/中间表自身的子表)按真实深度递归, + // 否则其子表的 superTable 永远是空壳,orm 读取时会 NPE + int childLevel = ismain ? 0 : level + 1; + linkEntityColumn.getEntityLinkRelation().setSuperTable(getEntityTable(false, linkEntityColumn.getEntityLinkRelation().getRelationName(), childLevel, visiting)); } } 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(); // List onlFormFieldList = onlFormHeadService.onlFormFieldListCache(onlFormHead.getId()); // entityTable.setTableId(onlFormHead.getId()); diff --git a/common/common-generator/src/main/java/apelet/common/generator/utils/cache/MetaCacheService.java b/common/common-generator/src/main/java/apelet/common/generator/utils/cache/MetaCacheService.java index 30cebf4..ba2d381 100644 --- a/common/common-generator/src/main/java/apelet/common/generator/utils/cache/MetaCacheService.java +++ b/common/common-generator/src/main/java/apelet/common/generator/utils/cache/MetaCacheService.java @@ -8,10 +8,7 @@ import apelet.common.generator.model.OnlFormField; import apelet.common.generator.model.OnlFormHead; import apelet.common.generator.service.IOnlFormFieldService; import apelet.common.generator.service.IOnlFormHeadService; -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 apelet.common.orm.impl.*; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import org.redisson.api.RBucket; import org.redisson.api.RedissonClient; @@ -100,6 +97,20 @@ public class MetaCacheService { 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.setModelName(onlFormHead.getTableName()); - for (int i = 0; i < onlFormFieldList.size(); i++) { - OnlFormField onlFormField = onlFormFieldList.get(i); + for (OnlFormField onlFormField : onlFormFieldList) { 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()); entityColumn = new LinkEntityColumn(); EntityLinkRelation entityLinkRelation = new EntityLinkRelation(); @@ -136,8 +162,8 @@ public class MetaCacheService { dictFilter.setItemId(onlFormField.getFieldType()); GlobalDictItem globalDictItem = globalDictItemService.getOne(dictFilter); entityColumn.setColumnType(JDBCType.valueOf( - globalDictItem.getItemName().toUpperCase().equalsIgnoreCase("DATETIME") -?"TIMESTAMP":globalDictItem.getItemName().toUpperCase() + globalDictItem.getItemName().equalsIgnoreCase("DATETIME") + ? "TIMESTAMP" : globalDictItem.getItemName().toUpperCase() ).getVendorTypeNumber()); entityTable.getEntityColumnList().add(entityColumn); } diff --git a/common/common-online/pom.xml b/common/common-online/pom.xml index d28f2a9..6ada059 100644 --- a/common/common-online/pom.xml +++ b/common/common-online/pom.xml @@ -76,7 +76,7 @@ apelet common-orm - 1.0.3 + 1.0.4 diff --git a/common/common-online/src/main/java/apelet/common/online/abstractplugin/ListDataOrmService.java b/common/common-online/src/main/java/apelet/common/online/abstractplugin/ListDataOrmService.java index f97a3c0..8038507 100644 --- a/common/common-online/src/main/java/apelet/common/online/abstractplugin/ListDataOrmService.java +++ b/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.orm.impl.*; import apelet.common.orm.parser.FilterParser; +import apelet.common.online.util.WidgetFieldTypeMapping; import apelet.common.online.util.WidgetJsonUtil; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollUtil; @@ -386,6 +387,13 @@ public class ListDataOrmService extends ListDataService { OnlineColumn onlineColumn = columnMap.get(columnId); if (onlineColumn == null) return; 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 字段 String string = jsonObject.getJSONObject("props").getJSONObject("relativeTable").getString("displayField"); selectorSet.add(onlineColumn.getColumnName().toLowerCase() + "." + "id"); diff --git a/common/common-online/src/main/java/apelet/common/online/controller/OnlineFormController.java b/common/common-online/src/main/java/apelet/common/online/controller/OnlineFormController.java index cbe1474..ad0d9f5 100644 --- a/common/common-online/src/main/java/apelet/common/online/controller/OnlineFormController.java +++ b/common/common-online/src/main/java/apelet/common/online/controller/OnlineFormController.java @@ -16,6 +16,7 @@ import apelet.common.core.util.MyModelUtil; import apelet.common.core.validator.UpdateGroup; import apelet.common.dbutil.object.SqlTable; import apelet.common.dbutil.object.SqlTableColumn; +import apelet.common.generator.dto.OnlFormHeadDto; import apelet.common.generator.model.OnlFormField; import apelet.common.generator.model.OnlFormHead; 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.vo.OnlineFormVo; import apelet.common.sequence.wrapper.IdGeneratorWrapper; +import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.ObjectUtil; @@ -290,6 +292,8 @@ public class OnlineFormController { onlineFormDto.setWidgetJson(updatedJson.toJSONString()); } } + // ★ 多选关联(404):创建/复用中间表并回写绑定字段(前置:锚点字段已由 syncNewFieldsAndSubTables 创建,字段不存在则不处理) + syncMultiRelationTables(onlineForm, dataSource); if (!onlineFormService.update(onlineForm, originalOnlineForm, datasourceIdSet)) { redissonClient.getBucket(OnlineRedisKeyUtil.makeOnlineFormKey(onlineForm.getFormId())).delete(); return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); @@ -736,9 +740,8 @@ public class OnlineFormController { field.setCreateUserId(tokenData.getUserId()); field.setUpdateTime(now); field.setUpdateUserId(tokenData.getUserId()); - onlineQuickFormFieldService.save(field); } - + onlineQuickFormFieldService.saveBatch(fieldList); } // 3. 解析数据源与主表 OnlineDatasource datasource = onlineDatasourceService.getOnlineDatasourceByMasterTableId(dto.getMasterTableId()); @@ -849,12 +852,17 @@ public class OnlineFormController { if (CollUtil.isEmpty(fieldList) || columnIdMap == null || columnIdMap.isEmpty()) { return; } + List updateList = new ArrayList<>(); for (OnlineQuickFormField field : fieldList) { Long columnId = columnIdMap.get(field.getFieldName().toLowerCase()); - if (columnId != null && !Objects.equals(columnId, field.getColumnId())) { - field.setColumnId(columnId); - onlineQuickFormFieldService.updateById(field); + if (columnId == null || Objects.equals(columnId, field.getColumnId())) { + continue; } + field.setColumnId(columnId); + updateList.add(field); + } + if (CollUtil.isNotEmpty(updateList)) { + onlineQuickFormFieldService.updateBatchById(updateList); } } @@ -865,19 +873,48 @@ public class OnlineFormController { if (CollUtil.isEmpty(fieldList)) { return; } + // 先收集"是过滤条件且有过滤方案"的字段(同名字段后者覆盖,与原逐条更新语义一致),避免逐字段查列 + Map fieldFilterTypeMap = new HashMap<>(); for (OnlineQuickFormField field : fieldList) { if (field.getIsFilter() == null || field.getIsFilter() != 1 || field.getFilterType() == null || field.getFilterType() == FieldFilterType.NO_FILTER) { continue; } - OnlineColumn column = onlineColumnService.getOnlineColumnByTableIdAndColumnName(masterTableId, field.getFieldName()); - if (column != null && !Objects.equals(column.getFilterType(), field.getFilterType())) { - column.setFilterType(field.getFilterType()); - column.setUpdateTime(new Date()); - column.setUpdateUserId(tokenData.getUserId()); - onlineColumnService.updateById(column); - redissonClient.getBucket(OnlineRedisKeyUtil.makeOnlineTableKey(masterTableId)).delete(); + if (StringUtils.isBlank(field.getFieldName())) { + continue; + } + fieldFilterTypeMap.put(field.getFieldName().toLowerCase(), field.getFilterType()); + } + if (fieldFilterTypeMap.isEmpty()) { + return; + } + // 一次 IN 查询取回该表下目标列;历史脏数据可能同表同名列多条,取第一条(与原单条查询语义一致) + List columnList = onlineColumnService.list(new LambdaQueryWrapper().eq(OnlineColumn::getTableId, masterTableId).in(OnlineColumn::getColumnName, fieldFilterTypeMap.keySet())); + if (CollUtil.isEmpty(columnList)) { + return; + } + Map columnMap = new HashMap<>(); + for (OnlineColumn column : columnList) { + if (column.getColumnName() != null) { + columnMap.putIfAbsent(column.getColumnName().toLowerCase(), column); + } + } + Date now = new Date(); + List updateList = new ArrayList<>(); + for (Map.Entry 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回填 ============ @@ -890,6 +927,9 @@ public class OnlineFormController { if (CollUtil.isEmpty(fieldList)) { return; } + // ① 收集待解析字段与所需关联表单id,避免逐字段三次查询 + Map fieldFormIdMap = new LinkedHashMap<>(); + Set relatedFormIdSet = new LinkedHashSet<>(); for (OnlineQuickFormField field : fieldList) { if (field.getFieldType() == null || field.getFieldType() != 402) { continue; @@ -898,21 +938,43 @@ public class OnlineFormController { if (relatedFormId == null) { continue; } - OnlineForm relatedForm = onlineFormService.getById(relatedFormId); + fieldFormIdMap.put(field, relatedFormId); + relatedFormIdSet.add(relatedFormId); + } + if (relatedFormIdSet.isEmpty()) { + return; + } + // ② 批量取关联在线表单 + Map formMap = onlineFormService.listByIds(relatedFormIdSet).stream().collect(Collectors.toMap(OnlineForm::getFormId, Function.identity(), (a, b) -> a)); + Set relatedTableIdSet = formMap.values().stream().map(OnlineForm::getMasterTableId).filter(Objects::nonNull).collect(Collectors.toSet()); + // ③ 批量取关联主表 + Map tableMap = new HashMap<>(); + if (!relatedTableIdSet.isEmpty()) { + onlineTableService.listByIds(relatedTableIdSet).forEach(table -> tableMap.putIfAbsent(table.getTableId(), table)); + } + Set relatedTableNameSet = tableMap.values().stream().map(OnlineTable::getTableName).filter(StringUtils::isNotBlank).collect(Collectors.toSet()); + // ④ 批量取表头,同表名多条时取第一条 + Map headMap = new HashMap<>(); + if (!relatedTableNameSet.isEmpty()) { + onlFormHeadService.list(new LambdaQueryWrapper().in(OnlFormHead::getTableName, relatedTableNameSet)).forEach(head -> headMap.putIfAbsent(head.getTableName(), head)); + } + // ⑤ 内存回填 + for (Map.Entry entry : fieldFormIdMap.entrySet()) { + OnlineForm relatedForm = formMap.get(entry.getValue()); if (relatedForm == null || relatedForm.getMasterTableId() == null) { continue; } - OnlineTable relatedTable = onlineTableService.getById(relatedForm.getMasterTableId()); + OnlineTable relatedTable = tableMap.get(relatedForm.getMasterTableId()); if (relatedTable == null) { continue; } - OnlFormHead relatedHead = onlFormHeadService.getOne(new LambdaQueryWrapper().eq(OnlFormHead::getTableName, relatedTable.getTableName())); + OnlFormHead relatedHead = headMap.get(relatedTable.getTableName()); if (relatedHead == null) { continue; } - field.setRefTableId(relatedHead.getId()); - field.setRefTableName(relatedTable.getTableName()); - field.setRefFormCode(relatedForm.getFormCode()); + entry.getKey().setRefTableId(relatedHead.getId()); + entry.getKey().setRefTableName(relatedTable.getTableName()); + entry.getKey().setRefFormCode(relatedForm.getFormCode()); } } @@ -1060,7 +1122,8 @@ public class OnlineFormController { // 已有字段:字段控件带 slaveTableId(关联组件)则更新 refPropert;不带(如字典下拉)则清空残留,避免实体表误按关联表强转 Map 报错 boolean isTableColumn = !widget.containsKey("bindData"); 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); if (isAssociation && StrUtil.isNotEmpty(slaveTableId)) { if (!Objects.equals(existingField.getRefPropert(), Long.valueOf(slaveTableId))) { @@ -1183,6 +1246,193 @@ public class OnlineFormController { } + // ============ 多选关联(404)中间表同步 ============ + + /** + * 扫描 widgetJson 中的多选关联控件(widgetType=404),为每个控件创建/复用其专用中间表,并把中间表 headId 回写绑定字段。 + *

+ * 前置条件:控件绑定的字段必须已存在于 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 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().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().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().eq(OnlineTable::getTableName, tableKey.toLowerCase())); + if (hostTable == null) { + return null; + } + return onlFormHeadService.getOne(new LambdaQueryWrapper().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().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(宿主行id)、obj_id(所选记录主键)。 + */ + private List buildRelationTableFields() { + List 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().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); + } + } + /** * 提取控件/子表列绑定的从表 id(slaveTableId)。 * 字段控件在 bindData 下;子表列对象直接挂在顶层。 diff --git a/common/common-online/src/main/java/apelet/common/online/controller/OnlineOperationController.java b/common/common-online/src/main/java/apelet/common/online/controller/OnlineOperationController.java index 349249e..af5c07c 100644 --- a/common/common-online/src/main/java/apelet/common/online/controller/OnlineOperationController.java +++ b/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.util.OnlineConstant; import apelet.common.online.util.OnlineOperationHelper; +import apelet.common.online.util.WidgetFieldTypeMapping; import apelet.common.online.util.WidgetJsonUtil; import apelet.common.online.vo.OnlineEventPluginExecuteVo; import apelet.common.orm.impl.*; @@ -1033,6 +1034,9 @@ public class OnlineOperationController { } } Selector selector = getSelector(selectorSet, masterTable,true); + // 404 多选字段(MultiLinkEntityColumn)的选中集合也以 ObjectCollection 挂在行上, + // 与分录集合区分开后放 rowData 回显,避免被当分录塞进 listData(entryIdMap 里查不到 key 会存成 "null")。 + Set multiLinkFieldSet = ormGenDataSourceUtil.getEntityTable(masterTable.getTableName()).getEntityColumnList().stream().filter(c -> c instanceof MultiLinkEntityColumn).map(c -> c.getColumnName().toLowerCase()).collect(Collectors.toSet()); // 附表排序(SQL 层):配置了 sortField 的分录表按 分录表名.字段 排序。 Sorter sorter = null; if (!entrySortFieldMap.isEmpty()) { @@ -1064,10 +1068,18 @@ public class OnlineOperationController { Map map = resultList.get(0); map.keySet().forEach(k -> { - if (map.get(k) instanceof ObjectCollection) { + if (map.get(k) instanceof ObjectCollection && multiLinkFieldSet.contains(k)) { + // 404 多选回显:选中记录集合转 List 放表头字段(rowData),前端按记录回填多选组件。 + List> 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); List> entryList = new ArrayList<>(); onlineOperationService.collectionToJson(entryCollection, entryList); + // 分录行内的 404 多选集合同样要展开:collectionToJson 只展开嵌套 ObjectValue(F7), + // ObjectCollection 会被原样塞进 JSON(bean 形状),前端拿不到选中记录数组。 + this.expandEntryMultiLinkFields(k, entryList); // 附表配置了 sortField 时已在 SQL 层排序,这里不再按 seq 内存排序。 if (!entrySortFieldMap.containsKey(k)) { String sort = "seq"; @@ -1136,6 +1148,13 @@ public class OnlineOperationController { OnlineColumn onlineColumn = columnMap.get(columnId); if (onlineColumn == null) return; 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 字段 String string = jsonObject.getJSONObject("props").getJSONObject("relativeTable").getString("displayField"); selectorSet.add(onlineColumn.getColumnName().toLowerCase() + "." + "id"); @@ -1154,6 +1173,24 @@ public class OnlineOperationController { } } + /** + * 展开分录行内的 404 多选字段:按分录表 EntityTable 的 MultiLinkEntityColumn 列名定位, + * 把选中记录集合转成 List<Map>,供前端按选中记录回填多选组件。 + */ + private void expandEntryMultiLinkFields(String entryTableName, List> entryList) { + Set 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 row : entryList) { + multiLinkFields.forEach(field -> { + if (row.get(field) instanceof ObjectCollection) { + List> multiLinkList = new ArrayList<>(); + onlineOperationService.collectionToJson((ObjectCollection) row.get(field), multiLinkList); + row.put(field, multiLinkList); + } + }); + } + } + /** * 解析过滤表达式中的 ${字段名} 占位符,取值转成带引号的字符串(满足 FilterParser 值只接受字符串/null)。 diff --git a/common/common-online/src/main/java/apelet/common/online/service/impl/OnlineDatasourceServiceImpl.java b/common/common-online/src/main/java/apelet/common/online/service/impl/OnlineDatasourceServiceImpl.java index cfcd9f3..9c80814 100644 --- a/common/common-online/src/main/java/apelet/common/online/service/impl/OnlineDatasourceServiceImpl.java +++ b/common/common-online/src/main/java/apelet/common/online/service/impl/OnlineDatasourceServiceImpl.java @@ -251,8 +251,7 @@ public class OnlineDatasourceServiceImpl extends BaseService().eq(OnlineDatasource::getMasterTableId, masterTableId)); + return onlineDatasourceMapper.selectOne(new LambdaQueryWrapper().eq(OnlineDatasource::getMasterTableId, masterTableId), false); } @Override diff --git a/common/common-online/src/main/java/apelet/common/online/service/impl/OnlineFormServiceImpl.java b/common/common-online/src/main/java/apelet/common/online/service/impl/OnlineFormServiceImpl.java index 8b92056..702bcf1 100644 --- a/common/common-online/src/main/java/apelet/common/online/service/impl/OnlineFormServiceImpl.java +++ b/common/common-online/src/main/java/apelet/common/online/service/impl/OnlineFormServiceImpl.java @@ -26,6 +26,7 @@ import apelet.common.online.util.OnlineRedisKeyUtil; import apelet.common.orm.impl.EntityColumn; import apelet.common.orm.impl.EntityTable; import apelet.common.orm.impl.LinkEntityColumn; +import apelet.common.orm.impl.MultiLinkEntityColumn; import apelet.common.redis.util.CommonRedisUtil; import apelet.common.sequence.wrapper.IdGeneratorWrapper; import cn.hutool.core.collection.CollUtil; @@ -536,7 +537,26 @@ public class OnlineFormServiceImpl extends BaseService impleme public void setEntry(ObjectValue objectValue, @NotNull EntityTable entityTable, Map tableData) { for (EntityColumn entityColumn : entityTable.getEntityColumnList()) { 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(); Object obj = tableData.get(field_name); if (obj == null || obj.toString().equals("")) continue; @@ -561,4 +581,47 @@ public class OnlineFormServiceImpl extends BaseService impleme } } + /** + * 把多选关联控件的入参值统一成可遍历的元素集合。 + * 前端可能传 JSON 数组(正常)或单个对象/标量(单选误配),此处统一兼容。 + * + * @param obj 入参原始值。 + * @return 元素列表。 + */ + private List toElementList(Object obj) { + if (obj instanceof Collection) { + return new ArrayList<>((Collection) obj); + } + List 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; + } + } + } diff --git a/common/common-online/src/main/java/apelet/common/online/util/OrmOnlineDataSourceUtil.java b/common/common-online/src/main/java/apelet/common/online/util/OrmOnlineDataSourceUtil.java index cfccfb6..cd4fc3b 100644 --- a/common/common-online/src/main/java/apelet/common/online/util/OrmOnlineDataSourceUtil.java +++ b/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.online.dao.OnlineDatasourceTableMapper; import apelet.common.online.model.*; -import apelet.common.online.service.*; -import apelet.common.orm.impl.*; +import apelet.common.online.service.OnlineColumnService; +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 org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; diff --git a/common/common-online/src/main/java/apelet/common/online/util/WidgetFieldTypeMapping.java b/common/common-online/src/main/java/apelet/common/online/util/WidgetFieldTypeMapping.java index 3d1d752..583a06d 100644 --- a/common/common-online/src/main/java/apelet/common/online/util/WidgetFieldTypeMapping.java +++ b/common/common-online/src/main/java/apelet/common/online/util/WidgetFieldTypeMapping.java @@ -5,6 +5,15 @@ import java.util.Map; 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() { } @@ -64,6 +73,8 @@ public class WidgetFieldTypeMapping { WIDGET_TO_FIELD_TYPE.put(401, "2"); // 关联选择 → bigint WIDGET_TO_FIELD_TYPE.put(402, "2"); + // 关联多选 → bigint(绑定字段仅作锚点列,不存多选值,多选集合存专用中间表) + WIDGET_TO_FIELD_TYPE.put(404, "2"); // 时间选择器 → datetime WIDGET_TO_FIELD_TYPE.put(652, "3"); // 文件上传 → varchar diff --git a/common/common-online/src/main/java/apelet/common/online/util/WidgetJsonUtil.java b/common/common-online/src/main/java/apelet/common/online/util/WidgetJsonUtil.java index e7ceec6..45af6c4 100644 --- a/common/common-online/src/main/java/apelet/common/online/util/WidgetJsonUtil.java +++ b/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.JSONObject; import jodd.util.StringUtil; +import lombok.Getter; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.function.Consumer; 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 collectMultiRelationWidgets(JSONObject widgetJson) { + List multiRelationWidgetList = new ArrayList<>(); + if (widgetJson == null) { + return multiRelationWidgetList; + } + for (Map.Entry> tableEntry : buildTableFieldMap(widgetJson).entrySet()) { + for (Map.Entry 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); + } + + /** + * 读取多选关联控件「中间表」元数据所需的两个关联 id:F7 关联表(目标表)headId 与所属表 tableId。 + * 取值位置按控件形状区分: + *
    + *
  • 主表单字段控件:bindData.slaveTableId(目标表)、bindData.tableId(所属表)
  • + *
  • 分录表格列(无 bindData):顶层 slaveTableId / tableId
  • + *
+ * + * @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 关联目标表 headId(bindData.slaveTableId / 列顶层 slaveTableId),用于回写 onl_form_field.refPropert。 + */ + private final Long slaveTableId; + + /** + * F7 关联目标表物理名(bindData.slaveTableName),slaveTableId 取不到目标表时的兜底定位依据。 + */ + private final String slaveTableName; + + /** + * 所属表 tableId(bindData.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 的 widgetList(叶子字段控件),递归 childWidgetList 下钻,与 buildTableFieldMap 取值一致; * 一个列名可对应多个控件(多个控件绑定同一列)时返回多个 variableName。 @@ -496,23 +680,32 @@ public class WidgetJsonUtil { } for (int i = 0; i < tableColumnList.size(); 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 不做字段的搜集 continue; } Integer fieldType = tableColumn.getInteger("fieldType"); - if (fieldType != null && fieldType == 1) continue; - String columnIdStr = tableColumn.getString("columnId"); + if (fieldType != null && fieldType == 1){ continue;} String columnName; - if (StrUtil.isNotEmpty(columnIdStr) && !columnIdStr.matches("\\d+")) { - // 新增列:字段名存在 columnId 里(非数字),描述在 showName + if (isNewColumn) { columnName = columnIdStr; } else { // 已有列:字段名在 showFieldName,兜底 showName columnName = tableColumn.getString("showFieldName"); 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); } }