From 05530f7e7c850ed9efa5ae33ae7ad139c870c9b5 Mon Sep 17 00:00:00 2001 From: chenchuchuan <13554600537@163.com> Date: Fri, 14 Aug 2026 15:26:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(generator):=20=E6=B7=BB=E5=8A=A0=E5=9C=A8?= =?UTF-8?q?=E7=BA=BF=E8=A1=A8=E5=8D=95=E8=A7=84=E5=88=99=E5=BC=95=E6=93=8E?= =?UTF-8?q?=E5=8F=8A=E8=84=9A=E6=9C=AC=E4=BA=8B=E4=BB=B6=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在OnlFormHeadServiceImpl中添加seq字段用于排序顺序控制 - 新增AbstractRuleActionExecutor基类提供Vo产出助手功能 - 实现BaseDataCarryExecutor支持携带基础资料功能 - 新增ConditionEvaluator实现规则条件求值逻辑 - 在EventEnum中添加SCRIPT_EVENT脚本事件类型 - 定义EventTypeEnum归一化事件类型枚举 - 扩展ExecutePluginParent添加scriptEvent方法 - 实现FieldAssignExecutor支持设置字段值功能 - 新增FieldClearExecutor实现清除字段值功能 - 创建FormRule模型定义规则结构 - 构建FormRuleEngine作为规则引擎核心入口 - 实现FormRuleParser解析规则配置 - 开发FormulaAssignExecutor处理计算赋值逻辑 --- .../service/impl/OnlFormHeadServiceImpl.java | 9 + .../online/abstractplugin/ExecutePluginParent.java | 10 + .../abstractplugin/MyInterfaceRulesExecute.java | 311 --------------------- .../online/controller/OnlineFormController.java | 23 ++ .../controller/OnlineOperationController.java | 8 +- .../common/online/model/constant/EventEnum.java | 1 + .../apelet/common/online/rule/FormRuleEngine.java | 125 +++++++++ .../online/rule/RuleActionExecutorRegistry.java | 39 +++ .../rule/action/AbstractRuleActionExecutor.java | 105 +++++++ .../online/rule/action/RuleActionExecutor.java | 21 ++ .../rule/action/impl/BaseDataCarryExecutor.java | 105 +++++++ .../rule/action/impl/FieldAssignExecutor.java | 97 +++++++ .../rule/action/impl/FieldClearExecutor.java | 97 +++++++ .../rule/action/impl/FormulaAssignExecutor.java | 162 +++++++++++ .../rule/action/impl/WidgetHideExecutor.java | 27 ++ .../rule/action/impl/WidgetLockExecutor.java | 51 ++++ .../rule/action/impl/WidgetShowExecutor.java | 27 ++ .../rule/action/impl/WidgetUnlockExecutor.java | 27 ++ .../online/rule/condition/ConditionEvaluator.java | 121 ++++++++ .../online/rule/condition/FunctionExecutor.java | 67 +++++ .../common/online/rule/enums/EventTypeEnum.java | 26 ++ .../online/rule/enums/RuleActionTypeEnum.java | 44 +++ .../common/online/rule/enums/RuleTypeEnum.java | 23 ++ .../apelet/common/online/rule/model/FormRule.java | 32 +++ .../common/online/rule/model/RuleAction.java | 45 +++ .../online/rule/model/RuleActionContext.java | 31 ++ .../common/online/rule/model/RuleCondition.java | 29 ++ .../common/online/rule/model/RuleEvalContext.java | 19 ++ .../common/online/rule/parser/FormRuleParser.java | 110 ++++++++ .../service/IOnlineFormFieldSyncService.java | 21 ++ .../impl/OnlineFormFieldSyncServiceImpl.java | 208 ++++++++++++++ .../online/service/impl/OnlineFormServiceImpl.java | 252 +++++++---------- .../java/apelet/common/online/util/Jexl3Util.java | 81 +++++- .../apelet/common/online/util/WidgetJsonUtil.java | 51 ++++ 34 files changed, 1938 insertions(+), 467 deletions(-) delete mode 100644 common/common-online/src/main/java/apelet/common/online/abstractplugin/MyInterfaceRulesExecute.java create mode 100644 common/common-online/src/main/java/apelet/common/online/rule/FormRuleEngine.java create mode 100644 common/common-online/src/main/java/apelet/common/online/rule/RuleActionExecutorRegistry.java create mode 100644 common/common-online/src/main/java/apelet/common/online/rule/action/AbstractRuleActionExecutor.java create mode 100644 common/common-online/src/main/java/apelet/common/online/rule/action/RuleActionExecutor.java create mode 100644 common/common-online/src/main/java/apelet/common/online/rule/action/impl/BaseDataCarryExecutor.java create mode 100644 common/common-online/src/main/java/apelet/common/online/rule/action/impl/FieldAssignExecutor.java create mode 100644 common/common-online/src/main/java/apelet/common/online/rule/action/impl/FieldClearExecutor.java create mode 100644 common/common-online/src/main/java/apelet/common/online/rule/action/impl/FormulaAssignExecutor.java create mode 100644 common/common-online/src/main/java/apelet/common/online/rule/action/impl/WidgetHideExecutor.java create mode 100644 common/common-online/src/main/java/apelet/common/online/rule/action/impl/WidgetLockExecutor.java create mode 100644 common/common-online/src/main/java/apelet/common/online/rule/action/impl/WidgetShowExecutor.java create mode 100644 common/common-online/src/main/java/apelet/common/online/rule/action/impl/WidgetUnlockExecutor.java create mode 100644 common/common-online/src/main/java/apelet/common/online/rule/condition/ConditionEvaluator.java create mode 100644 common/common-online/src/main/java/apelet/common/online/rule/condition/FunctionExecutor.java create mode 100644 common/common-online/src/main/java/apelet/common/online/rule/enums/EventTypeEnum.java create mode 100644 common/common-online/src/main/java/apelet/common/online/rule/enums/RuleActionTypeEnum.java create mode 100644 common/common-online/src/main/java/apelet/common/online/rule/enums/RuleTypeEnum.java create mode 100644 common/common-online/src/main/java/apelet/common/online/rule/model/FormRule.java create mode 100644 common/common-online/src/main/java/apelet/common/online/rule/model/RuleAction.java create mode 100644 common/common-online/src/main/java/apelet/common/online/rule/model/RuleActionContext.java create mode 100644 common/common-online/src/main/java/apelet/common/online/rule/model/RuleCondition.java create mode 100644 common/common-online/src/main/java/apelet/common/online/rule/model/RuleEvalContext.java create mode 100644 common/common-online/src/main/java/apelet/common/online/rule/parser/FormRuleParser.java create mode 100644 common/common-online/src/main/java/apelet/common/online/service/IOnlineFormFieldSyncService.java create mode 100644 common/common-online/src/main/java/apelet/common/online/service/impl/OnlineFormFieldSyncServiceImpl.java 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 bb63a20..89d50d9 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 @@ -781,6 +781,15 @@ public class OnlFormHeadServiceImpl extends ServiceImpl> conditional = (List) values.get("conditional"); - conditional("加载", widgetVariableName, conditional, objectValue); - List> business = (List) values.get("business"); - conditional("加载", widgetVariableName, business, objectValue); - } - - @Override - public void change(String widgetVariableName, ObjectValue objectValue) { - Map values = objectValue.getValues(); - List> conditional = (List) values.get("conditional"); - conditional("值更新", widgetVariableName, conditional, objectValue); - List> business = (List) values.get("business"); - conditional("值更新", widgetVariableName, business, objectValue); - } - - void conditional(String type, String widgetVariableName, List> isEstablish, ObjectValue objectValue) { - if (isEstablish == null || objectValue == null) { - return; - } - for (Map obj : isEstablish) { - List allList = (List) obj.get("allList"); - List condition; - if ((boolean) obj.get("establish")) { - condition = allList.stream().filter(f -> "1".equals(f.get("condition").toString())).collect(Collectors.toList()); - } else { - condition = allList.stream().filter(f -> "2".equals(f.get("condition").toString())).collect(Collectors.toList()); - } - //业务规则 - d(type, widgetVariableName, condition, objectValue); - - // 条件规则 - b(type, widgetVariableName, condition); - } - } - - void d(String pluginType, String widgetVariableName, List jsonArray, ObjectValue objectValue) { - for (JSONObject obj : jsonArray) { - String checkList = obj.getJSONArray("checkList").toString(); - if (checkList.contains(pluginType)) { - String type = obj.get("type").toString(); - JSONArray treeList = obj.getJSONArray("treeList"); - switch (type) { - case "1": { //计算定义公式的值并填写到指定列 - String expressionTransl = obj.getString("expressionTranslation"); - List split = Arrays.asList(expressionTransl.split("=")); - String fun = ""; - for (String s : split) { - s = s.trim(); - if (s.contains("$")) { - fun = s; - } - } - if (StringUtils.isEmpty(fun)) { - String tag = split.get(0); - String src = split.get(1); - if (src.contains(objectValue.getTableName())) { - // 公式引用表单字段(如 amount = price * qty):把 表.表.字段 引用替换为实际值后求值,再赋给目标字段 - Map context = new HashMap<>(); - for (String ref : src.split("[^a-zA-Z0-9_.]+")) { - if (!ref.contains(objectValue.getTableName()) || context.containsKey(ref)) { - continue; - } - context.put(ref, this.resolveFormulaField(ref, objectValue)); - } - Object computed = jexl3Util.expression(src, context); - List list = Arrays.asList(tag.split("\\.")); - super.setWidgetAttribute(list.get(list.size() - 1).trim(), AttributeEnum.DATA_VALUE_CHANGE, computed); - } else { - List list = Arrays.asList(tag.split("\\.")); - super.setWidgetAttribute(list.get(list.size() - 1), AttributeEnum.DATA_VALUE_CHANGE, src); - } - } else { - fun = jexl3Util.getMySplit("\\$(.*?)\\$", fun); - List funParam = Arrays.asList(jexl3Util.getMySplit("\\((.*?)\\)", fun).split(",")); - Object[] objects = jexl3Util.getFunParam(funParam, objectValue); - Object o = onlineFormService.executeFunction(obj, fun, objects); - List list = Arrays.asList(split.get(0).split("\\.")); - super.setWidgetAttribute(list.get(list.size() - 1).trim(), AttributeEnum.DATA_VALUE_CHANGE, o); - } - break; - } - case "2": { //携带基础资料属性到指定列 - JSONArray array = obj.getJSONArray("specifyJiList"); - JSONObject keyNameObj = (JSONObject) objectValue.get("keyNameObj"); - for (Object o : array) { - JSONObject o2 = (JSONObject) o; - String[] sourceFields = o2.get("sourceFields").toString().split("\\."); - String[] targetFields = o2.get("targetFields").toString().split("\\."); - if (sourceFields[0].equals(sourceFields[1])) { - ObjectValue o1 = (ObjectValue) objectValue.get(sourceFields[2]); - Object o11 = o1.get("id"); - if (o11 != null) { - ObjectValue next1 = jexl3Util.getObjectValue(o1.get("id"), o1.getTableName()); - Object values = next1.get(sourceFields[3]); - super.setWidgetAttribute(StrUtil.toCamelCase(targetFields[1]), AttributeEnum.VALUE_CHANGE, values); - } - } else { - ObjectCollection o3 = (ObjectCollection) objectValue.get(sourceFields[1]); - Iterator iterator = o3.iterator(); - String rowKeySubset = getDto().getRowKeySubset(); - Set targetSet = new HashSet(); - if (rowKeySubset != null && !rowKeySubset.equals("")) { - String[] rowKeySubsetAry = rowKeySubset.split(","); - Collections.addAll(targetSet, rowKeySubsetAry); - - } - while (iterator.hasNext()) { - ObjectValue next = (ObjectValue) iterator.next(); - String deviceType = AppDeviceType.getDeviceType(); - if ( - (StringUtils.isEmpty(rowKeySubset) && next.get("rowkey") != null) || //分录弹出界面新增时,rowkey 为空,所以为空时,为当前编辑数据 - (next.get("rowkey") != null && !targetSet.contains(next.get("rowkey"))) //rowKey相等时,为当前编辑数据 - ) { - continue; - } - ObjectValue o4 = (ObjectValue) next.get(sourceFields[2]); - if (o4 == null) { - return; - } - o4 = jexl3Util.getObjectValue(o4.get("id"), o4.getTableName()); - Object values = o4.get(sourceFields[3]); - Map rowChangeMap = (Map) objectValue.get("rowChange"); - if (rowChangeMap == null) {//为空是弹出界面编辑,不为空行内编辑 - super.setWidgetAttribute(StrUtil.toCamelCase(targetFields[1]), AttributeEnum.VALUE_CHANGE, values); - } else { - super.setWidgetAttributeByEntry(widgetVariableName, targetFields[1], AttributeEnum.VALUE_CHANGE, values, - next.getString("rowkey")); - } - } - } - } - break; - } - case "3": { //设置当前编辑字段值到指定字段 - JSONArray array = obj.getJSONArray("specifyList"); - Object v = ""; - for (Object o : array) { - JSONObject o2 = (JSONObject) o; - String sourceFields = (String) o2.get("sourceFields"); - List list = Arrays.asList(sourceFields.split("\\.")); - String filed = list.get(list.size() - 1); - String table = list.get(list.size() - 2); - if (table.equals(objectValue.getTableName())) { - v = objectValue.get(filed); - } else { - ObjectCollection o1 = (ObjectCollection) objectValue.get(table); - Iterator iterator = o1.iterator(); - while (iterator.hasNext()) { - ObjectValue next = (ObjectValue) iterator.next(); - v = next.get(String.valueOf(filed)); - } - } - String targetFields = (String) o2.get("targetFields"); - List list1 = Arrays.asList(targetFields.split("\\.")); - super.setWidgetAttribute(list1.get(list1.size() - 1), AttributeEnum.VALUE_CHANGE, v); - } - break; - } - case "4": { //清除指定字段值 - for (Object o : treeList) { - JSONObject jsonObject = (JSONObject) o; - if (jsonObject.containsKey("zxName")) { - String[] split = jsonObject.getString("zxName").split("\\."); - if (split[0].equals(split[1])) { - String name = jsonObject.get("label").toString(); - super.setWidgetAttribute(name, AttributeEnum.VALUE_CHANGE, ""); - } else { - String tab = jexl3Util.getMySplit("\\((.*?)\\)", split[1]); - JSONObject keyNameObj = (JSONObject) objectValue.get("keyNameObj"); - String tabId = (String) keyNameObj.getJSONArray(tab).get(0); - ObjectCollection collection = (ObjectCollection) objectValue.get(tab); - Iterator iterator = collection.iterator(); - while (iterator.hasNext()) { - ObjectValue next = (ObjectValue) iterator.next(); - super.setWidgetAttributeByEntry(tabId, jsonObject.get("label").toString(), AttributeEnum.VALUE_CHANGE, "", next.getString("rowkey")); - } - } - } - } - break; - } - } - } - } - } - - void b(String pluginType, String widgetVariableName, List jsonArray) { - for (JSONObject obj : jsonArray) { - String checkList = obj.getJSONArray("checkList").toString(); - if (checkList.contains(pluginType)) { - String type = obj.get("type").toString(); - JSONArray treeList = obj.getJSONArray("treeList"); - JSONArray objects = new JSONArray(); - switch (type) { - case "5": { //锁定控件 - for (Object o : treeList) { - JSONObject jsonObject = (JSONObject) o; - String name = jsonObject.get("variableName").toString(); - if (jsonObject.containsKey("btnId")) { - String showName = jsonObject.get("showName").toString(); - String parentVariableName = jsonObject.get("parentVariableName").toString(); - JSONObject json = new JSONObject(); - json.put("name", showName); - json.put("disable", true); - objects.add(json); - super.setWidgetAttribute(parentVariableName, AttributeEnum.CHANGEOPERATE, objects); - } else { - super.setWidgetAttribute(name, AttributeEnum.DISABLED, true); - } - } - break; - } - case "6": { //解锁控件 - for (Object o : treeList) { - JSONObject jsonObject = (JSONObject) o; - String name = jsonObject.get("variableName").toString(); - super.setWidgetAttribute(name, AttributeEnum.DISABLED, false); - } - break; - } - case "7": { //显示控件 - for (Object o : treeList) { - JSONObject jsonObject = (JSONObject) o; - String name = jsonObject.get("variableName").toString(); - super.setWidgetAttribute(name, AttributeEnum.SHOW, true); - } - break; - } - case "8": { //隐藏控件 - for (Object o : treeList) { - JSONObject jsonObject = (JSONObject) o; - String name = jsonObject.get("variableName").toString(); - super.setWidgetAttribute(name, AttributeEnum.SHOW, false); - } - break; - } - } - } - } - } - - - @Override - public void beforeSelector(String widgetVariableName, ObjectValue objectValue) { - super.beforeSelector(widgetVariableName, objectValue); - } - - @Override - public void entryEditComplete(String widgetVariableName, ObjectValue objectValue) { - super.entryEditComplete(widgetVariableName, objectValue); - } - - public String removeSymbols(String input) { - // 使用正则表达式替换所有非字母数字字符为空字符串 - return input.replaceAll("_", "").toLowerCase(); - } - - /** - * 解析公式中 表.表.字段 引用的实际值。 - * 主表字段直接取值;关联字段(表.表.关联列.子字段)取子对象属性;分录字段取第一行值;空值统一补 0。 - */ - private Object resolveFormulaField(String ref, ObjectValue objectValue) { - String[] parts = ref.split("\\."); - Object value; - if (parts[0].equals(parts[1])) { - if (parts.length > 3) { - Object linked = objectValue.get(parts[2]); - value = linked instanceof ObjectValue ? ((ObjectValue) linked).get(parts[3]) : null; - } else { - value = objectValue.get(parts[2]); - } - } else { - ObjectCollection collection = (ObjectCollection) objectValue.get(parts[1]); - value = (collection == null || collection.isEmpty()) - ? null : ((ObjectValue) collection.iterator().next()).get(parts[2]); - } - return value == null || value.toString().isEmpty() ? 0 : value; - } -} 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 83fdfd2..0d7d29d 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 @@ -97,6 +97,8 @@ public class OnlineFormController { @Autowired OnlinePageService onlinePageService; @Autowired + private IOnlineFormFieldSyncService onlineFormFieldSyncService; + @Autowired private OnlineProperties properties; @Resource(name = "caffeineCacheManager") private CacheManager cacheManager; @@ -108,6 +110,25 @@ public class OnlineFormController { private FormButtonPermissionChecker buttonPermissionChecker; /** + * 自动同步字段到历史数据:给所有缺该字段的 onl_form_head 表补字段(可按表类型过滤)。 + * 已同步的表补字段+同步数据库+回写 zz_online_column;未同步的表只补字段。 + * + * @param fieldName 字段名 + * @param type 字段类型(数据库类型名如 varchar/bigint/decimal,或 onl_fieldType 字典码 0~5) + * @param length 字段长度(varchar 默认 255;decimal 为精度、小数位默认 2) + * @param description 字段描述 + * @param tableType 表类型(0 附表 / 1 主表 / 2 单表,不传则全部) + */ + @GetMapping(value = "/syncFieldToHistory") + public ResponseResult syncFieldToHistory(@RequestParam String fieldName, + @RequestParam String type, + @RequestParam(required = false) Integer length, + @RequestParam(required = false) String description, + @RequestParam(required = false) String tableType) { + return ResponseResult.success(onlineFormFieldSyncService.syncFieldToHistory(fieldName, type, length, description, tableType)); + } + + /** * 新增在线表单数据。 * * @param onlineFormDto 新增对象。 @@ -718,6 +739,8 @@ public class OnlineFormController { return false; } for (String subTableName : newSubTableNames) { + // 因为是附表,所以要加上 + subTableName = "t_" + subTableName + "_entry"; OnlFormHead subHead = onlFormHeadService.createMetaAndSync(subTableName, onlFormHead.getTableTxt() + "附表", CustomUtil.MetaType_Entry, onlFormHead.getTableName()); // 获取子表的 OnlineTable OnlineTable subOnlineTable = onlineTableService.getOne(new LambdaQueryWrapper().eq(OnlineTable::getTableName, subTableName.toLowerCase())); 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 ca3a766..536f7ce 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 @@ -971,7 +971,7 @@ public class OnlineOperationController { HashMap rowData = new HashMap<>(); HashMap listData = new HashMap<>(); ObjectCollection collection = ormGenDataSourceUtil.query(masterTable.getTableName(), filter, selector); - if(collection.size() == 0){ + if (collection.isEmpty()) { JSONObject jsonObject = new JSONObject(); rowData.put("id", id); jsonObject.put("rowData", rowData); @@ -991,6 +991,12 @@ public class OnlineOperationController { ObjectCollection entryCollection = (ObjectCollection) map.get(k); List> entryList = new ArrayList<>(); onlineOperationService.collectionToJson(entryCollection, entryList); + String sort = "seq"; + entryList = entryList.stream().sorted( + Comparator.comparing(m -> m.containsKey(sort) && m.get(sort) != null && StringUtils.isNotBlank(m.get(sort).toString()) + ? Double.valueOf(m.get(sort).toString()) : null, + Comparator.nullsLast(Double::compareTo))) + .collect(Collectors.toList()); listData.put(String.valueOf(entryIdMap.get(k)), entryList); } else { rowData.put(k, map.get(k)); diff --git a/common/common-online/src/main/java/apelet/common/online/model/constant/EventEnum.java b/common/common-online/src/main/java/apelet/common/online/model/constant/EventEnum.java index 5ddbbd5..a452c4e 100644 --- a/common/common-online/src/main/java/apelet/common/online/model/constant/EventEnum.java +++ b/common/common-online/src/main/java/apelet/common/online/model/constant/EventEnum.java @@ -42,6 +42,7 @@ public enum EventEnum { MENU_CLICK("menuClick", "菜单点击事件"), OPERATION_AFTER("operationAfter", "保存后"), TIMER_ELAPSED("timerElapsed", "前端定时发送请求,触发此界面事件。"), + SCRIPT_EVENT("scriptEvent", "脚本事件"), ; diff --git a/common/common-online/src/main/java/apelet/common/online/rule/FormRuleEngine.java b/common/common-online/src/main/java/apelet/common/online/rule/FormRuleEngine.java new file mode 100644 index 0000000..d73318e --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/rule/FormRuleEngine.java @@ -0,0 +1,125 @@ +package apelet.common.online.rule; + +import apelet.common.core.object.ObjectValue; +import apelet.common.online.dto.OnlineEventPluginExecuteDto; +import apelet.common.online.model.OnlineForm; +import apelet.common.online.rule.action.RuleActionExecutor; +import apelet.common.online.rule.condition.ConditionEvaluator; +import apelet.common.online.rule.enums.RuleTypeEnum; +import apelet.common.online.rule.model.FormRule; +import apelet.common.online.rule.model.RuleAction; +import apelet.common.online.rule.model.RuleActionContext; +import apelet.common.online.rule.model.RuleEvalContext; +import apelet.common.online.rule.parser.FormRuleParser; +import com.alibaba.fastjson.JSONObject; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +/** + * 规则引擎入口:解析 → 规则级一次求值 → 按 condition 选动作 → 事件匹配 → 路由执行器。 + */ +@Component +public class FormRuleEngine { + + @Resource + private FormRuleParser parser; + @Resource + private ConditionEvaluator conditionEvaluator; + @Resource + private RuleActionExecutorRegistry registry; + + public List execute(OnlineForm form, ObjectValue ov, String eventCode, OnlineEventPluginExecuteDto dto) { + List result = new ArrayList<>(); + if (form == null || form.getWidgetJson() == null) { + return result; + } + for (FormRule rule : parser.parse(form.getWidgetJson())) { + executeRule(rule, form, ov, eventCode, dto, result); + } + return result; + } + + /** + * 通用动作执行入口:给定单条规则的 JSON 串、字段值、规则来源类型, + * 走 解析 → 条件求值 → 按 condition 选动作 → 路由执行器 → 产出 Vo 全流程。 + * 通用入口不按事件过滤(eventCode 为 null,checkList 只作展示,不拦截执行)。 + * + * @param ruleJson 单条规则 JSON 串(businessRulesList / conditionTypeList 数组元素:expressionTranslation、allList、Enable 等) + * @param values 字段取值(key 为字段名或完整 表.表.字段 / 表.关联对象.字段) + * @param ruleType 规则来源类型(界面规则 / 业务规则) + */ + public List executeAction(String ruleJson, Map values, RuleTypeEnum ruleType) { + if (StringUtils.isBlank(ruleJson)) { + return null; + } + FormRule rule = parser.parseSingle(JSONObject.parseObject(ruleJson), ruleType); + if (rule == null) { + return null; + } + List result = new ArrayList<>(); + executeRule(rule, null, buildObjectValue(rule.getCondition().getExpression(), values), null, + new OnlineEventPluginExecuteDto(), result); + return result; + } + + private void executeRule(FormRule rule, OnlineForm form, ObjectValue ov, String eventCode, + OnlineEventPluginExecuteDto dto, List result) { + if (!rule.isEnable()) { + return; + } + // 规则级:表达式只求值一次 + boolean establish = conditionEvaluator.evaluate(rule.getCondition(), new RuleEvalContext(ov, dto)); + // 规则内动作按 sort 升序执行(稳定排序,缺失/相同的保持数组顺序) + rule.getActions().sort(Comparator.comparingInt(RuleAction::getSort)); + for (RuleAction action : rule.getActions()) { + // 成立 → condition=1;不成立 → condition=2 + if ((establish && action.getCondition() != 1) || (!establish && action.getCondition() != 2)) { + continue; + } + // executeAction 通用入口 eventCode 为 null:不按事件过滤,全部执行 + if (eventCode != null && !action.matchEvent(eventCode)) { + continue; + } + RuleActionExecutor executor = registry.get(action.getActionType()); + RuleActionContext ctx = new RuleActionContext(form, ov, dto, action, eventCode, + dto == null ? null : dto.getEventWidgetVariableName(), rule.getCondition().getFunctionForm()); + Object value = executor.resolveValue(ctx); + result.addAll(executor.buildResult(ctx, value)); + } + } + + private ObjectValue buildObjectValue(String expression, Map values) { + ObjectValue objectValue = new ObjectValue(extractTableName(expression)); + if (values != null) { + for (Map.Entry entry : values.entrySet()) { + objectValue.put(lastSegment(entry.getKey()), entry.getValue()); + } + } + return objectValue; + } + + /** + * 从表达式里取主表名:如 "t_flowtest001.t_flowtest001.amount = ..." → "t_flowtest001" + */ + private String extractTableName(String expressionTranslation) { + if (StringUtils.isBlank(expressionTranslation)) return ""; + int eq = expressionTranslation.indexOf('='); + String tag = (eq < 0 ? expressionTranslation : expressionTranslation.substring(0, eq)).trim(); + int dot = tag.indexOf('.'); + return dot < 0 ? tag : tag.substring(0, dot); + } + + /** + * 取 key 末段:完整 表.表.字段 或字段名 → 字段名 + */ + private String lastSegment(String key) { + int dot = key.lastIndexOf('.'); + return dot < 0 ? key : key.substring(dot + 1); + } +} diff --git a/common/common-online/src/main/java/apelet/common/online/rule/RuleActionExecutorRegistry.java b/common/common-online/src/main/java/apelet/common/online/rule/RuleActionExecutorRegistry.java new file mode 100644 index 0000000..53776ba --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/rule/RuleActionExecutorRegistry.java @@ -0,0 +1,39 @@ +package apelet.common.online.rule; + +import apelet.common.online.rule.action.RuleActionExecutor; +import apelet.common.online.rule.enums.RuleActionTypeEnum; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 动作类型 → 执行器 路由选择器。 + * 常量 map 在构造器注入时一次性初始化,运行期只读,无需反复构建。 + * 加新动作类型 = 加一个执行器,Spring 注入后自动注册,无需改这里。 + */ +@Slf4j +@Component +public class RuleActionExecutorRegistry { + + /** 动作类型 → 执行器 常量映射,构造器初始化,运行期只读 */ + private final Map executors = new HashMap<>(); + + public RuleActionExecutorRegistry(List executorList) { + for (RuleActionExecutor executor : executorList) { + executors.put(executor.getActionType(), executor); + } + log.info("规则执行器初始化完成,共注册 {} 个动作类型:{}", executors.size(), executors.keySet()); + } + + /** 按动作类型路由执行器 */ + public RuleActionExecutor get(RuleActionTypeEnum actionType) { + RuleActionExecutor executor = executors.get(actionType); + if (executor == null) { + throw new IllegalArgumentException("未注册的动作类型: " + actionType); + } + return executor; + } +} diff --git a/common/common-online/src/main/java/apelet/common/online/rule/action/AbstractRuleActionExecutor.java b/common/common-online/src/main/java/apelet/common/online/rule/action/AbstractRuleActionExecutor.java new file mode 100644 index 0000000..a70057f --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/rule/action/AbstractRuleActionExecutor.java @@ -0,0 +1,105 @@ +package apelet.common.online.rule.action; + +import apelet.common.core.object.ObjectCollection; +import apelet.common.core.object.ObjectValue; +import apelet.common.online.model.OnlineForm; +import apelet.common.online.model.constant.AttributeEnum; +import apelet.common.online.rule.model.RuleActionContext; +import apelet.common.online.util.WidgetJsonUtil; +import apelet.common.online.vo.OnlineEventPluginExecuteEntryVo; +import apelet.common.online.vo.OnlineEventPluginExecuteVo; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * 执行器基类:自带 Vo 产出助手(不复用 ExecutePluginParent,插件机制独立)。 + */ +public abstract class AbstractRuleActionExecutor implements RuleActionExecutor { + + /** 默认取值:整表单(type5~8 无重活,直接用默认) */ + @Override + public Object resolveValue(RuleActionContext ctx) { + return ctx.getObjectValue(); + } + + protected OnlineEventPluginExecuteVo buildVo(String widgetVariableName, AttributeEnum type, Object value) { + OnlineEventPluginExecuteVo vo = new OnlineEventPluginExecuteVo(); + vo.setWidgetVariableName(widgetVariableName); + vo.setWidgetOperateType(type.getCode()); + vo.setWidgetOperateContent(value instanceof ObjectValue ? ((ObjectValue) value).getValues() : value); + return vo; + } + + protected OnlineEventPluginExecuteEntryVo buildEntryVo(String name, String col, AttributeEnum type, Object value, String rowNum) { + OnlineEventPluginExecuteEntryVo vo = new OnlineEventPluginExecuteEntryVo(); + vo.setWidgetVariableName(name); + vo.setEntriesTableColumnName(col); + vo.setWidgetOperateType(type.getCode()); + vo.setWidgetOperateContent(value); + vo.setRowNum(rowNum); + return vo; + } + + /** + * 列名 → 控件 variableName 列表:从表单 widgetJson 的 pc/mobile.widgetList 按 bindData.columnFieldName 反查。 + * 未配置映射的列名原样返回自身;一个列名对应多个控件(多个控件绑定同一列)时返回多个 variableName。 + */ + protected List resolveWidgetVariableNames(RuleActionContext ctx, String column) { + OnlineForm form = ctx.getForm(); + if (form != null && StringUtils.isNotBlank(form.getWidgetJson())) { + Map> map = WidgetJsonUtil.collectColumnVariableNames(JSONObject.parseObject(form.getWidgetJson())); + List names = map.get(column); + if (names != null && !names.isEmpty()) { + return names; + } + } + return Collections.singletonList(column); + } + + /** treeList 控件目标 → 一组普通指令(type 6/7/8 用) */ + protected List buildFromTreeList(RuleActionContext ctx, AttributeEnum type, Object attrValue) { + List out = new ArrayList<>(); + JSONArray treeList = ctx.getAction().getSource().getJSONArray("treeList"); + if (treeList == null) return out; + for (Object o : treeList) { + JSONObject json = (JSONObject) o; + out.add(buildVo(json.get("variableName").toString(), type, attrValue)); + } + return out; + } + + /** 分录 tabId:keyNameObj 反查(主表/已登记分录),缺失时兜底用事件触发控件名(分录内触发即分录表控件),均无则返回 null 不产出分录 Vo */ + protected String resolveEntryTabId(ObjectValue objectValue, String tableName, String eventWidgetVariableName) { + JSONObject keyNameObj = (JSONObject) objectValue.get("keyNameObj"); + if (keyNameObj != null) { + JSONArray tabIds = keyNameObj.getJSONArray(tableName); + if (tabIds != null && !tabIds.isEmpty()) { + return tabIds.get(0).toString(); + } + } + return eventWidgetVariableName; + } + + /** + * 从目标/源字段路径识别附表名:目标 主表.分录表.字段 / 分录表.字段 优先,源 主表.分录表.字段 兜底(附表内字段影响附表字段时目标随源); + * 主表目标(表.字段 / 表.表.字段)返回 null。 + */ + protected String resolveEntryTable(ObjectValue objectValue, String[] targetParts, String[] sourceParts) { + if (targetParts.length >= 3 && !targetParts[0].equals(targetParts[1]) && objectValue.get(targetParts[1]) instanceof ObjectCollection) { + return targetParts[1]; + } + if (targetParts.length == 2 && objectValue.get(targetParts[0]) instanceof ObjectCollection) { + return targetParts[0]; + } + if (sourceParts.length >= 3 && !sourceParts[0].equals(sourceParts[1]) && objectValue.get(sourceParts[1]) instanceof ObjectCollection) { + return sourceParts[1]; + } + return null; + } +} diff --git a/common/common-online/src/main/java/apelet/common/online/rule/action/RuleActionExecutor.java b/common/common-online/src/main/java/apelet/common/online/rule/action/RuleActionExecutor.java new file mode 100644 index 0000000..9acdea9 --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/rule/action/RuleActionExecutor.java @@ -0,0 +1,21 @@ +package apelet.common.online.rule.action; + +import apelet.common.online.rule.enums.RuleActionTypeEnum; +import apelet.common.online.rule.model.RuleActionContext; +import apelet.common.online.vo.OnlineEventPluginExecuteVo; + +import java.util.List; + +/** + * 规则动作执行器:一个动作类型一个执行器,三方法契约。 + */ +public interface RuleActionExecutor { + /** ① 路由标识:本执行器负责的动作类型,Registry 靠它路由 */ + RuleActionTypeEnum getActionType(); + + /** ② 取值:读取执行所需的字段/数据(各动作类型差异最大) */ + Object resolveValue(RuleActionContext ctx); + + /** ③ 返回结果:产出前端指令 */ + List buildResult(RuleActionContext ctx, Object value); +} diff --git a/common/common-online/src/main/java/apelet/common/online/rule/action/impl/BaseDataCarryExecutor.java b/common/common-online/src/main/java/apelet/common/online/rule/action/impl/BaseDataCarryExecutor.java new file mode 100644 index 0000000..ed4478a --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/rule/action/impl/BaseDataCarryExecutor.java @@ -0,0 +1,105 @@ +package apelet.common.online.rule.action.impl; + +import apelet.common.core.object.ObjectCollection; +import apelet.common.core.object.ObjectValue; +import apelet.common.online.model.constant.AttributeEnum; +import apelet.common.online.rule.action.AbstractRuleActionExecutor; +import apelet.common.online.rule.enums.RuleActionTypeEnum; +import apelet.common.online.rule.model.RuleActionContext; +import apelet.common.online.util.Jexl3Util; +import apelet.common.online.vo.OnlineEventPluginExecuteVo; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.*; + +/** + * type 2 携带基础资料:主表直接带出,分录按 rowKeySubset 只带当前编辑行(行内编辑带 rowNum)。 + */ +@Component +public class BaseDataCarryExecutor extends AbstractRuleActionExecutor { + + @Resource + private Jexl3Util jexl3Util; + + @Override + public RuleActionTypeEnum getActionType() { + return RuleActionTypeEnum.BASE_DATA_CARRY; + } + + @Override + public Object resolveValue(RuleActionContext ctx) { + List values = new ArrayList<>(); + ObjectValue objectValue = ctx.getObjectValue(); + JSONArray array = ctx.getAction().getSource().getJSONArray("specifyJiList"); + if (array == null) { + return values; + } + String rowKeySubset = ctx.getDto().getRowKeySubset(); + String rowKey = ctx.getDto().getRowKey(); + Set targetSet = new HashSet<>(); + if (StringUtils.isNotEmpty(rowKeySubset)) { + Collections.addAll(targetSet, rowKeySubset.split(",")); + } + Map rowChangeMap = (Map) objectValue.get("rowChange"); + for (Object o : array) { + JSONObject o2 = (JSONObject) o; + String[] parts = o2.get("sourceFields").toString().split("\\."); + String[] targetParts = o2.get("targetFields").toString().split("\\."); + String targetField = targetParts[targetParts.length - 1]; + Object first = objectValue.get(parts[1]); + if (!parts[0].equals(parts[1]) && first instanceof ObjectCollection) { // 分录:主表.分录表.关联对象.字段 + Iterator iterator = ((ObjectCollection) first).iterator(); + while (iterator.hasNext()) { + ObjectValue next = (ObjectValue) iterator.next(); + String k = next.getString("rowkey"); + if (StringUtils.isNotEmpty(rowKeySubset)) { // 行内编辑:命中 subset 的行 + if (k == null || !targetSet.contains(k)) continue; + } else if (StringUtils.isNotEmpty(rowKey)) { // 分录编辑完成:命中当前行 rowKey + if (!rowKey.equals(k)) continue; + } else { // 无过滤:跳过已保存行,只处理新增行 + if (k != null) continue; + } + String rowNum = rowChangeMap == null ? null : next.getString("rowkey"); // 行内编辑才带 rowNum + Object value = jexl3Util.resolveFieldChain(next, parts, 2); + next.put(targetField, value); // 写回分录行,供后续规则条件求值 + values.add(new CarryValue(targetField, value, rowNum)); + } + } else { // 主表:表.关联对象.子字段 / 表.表.字段 + Object value = jexl3Util.resolveFieldChain(objectValue, parts, parts[0].equals(parts[1]) ? 2 : 1); + objectValue.put(targetField, value); // 写回主表,供后续规则条件求值 + values.add(new CarryValue(targetField, value, null)); + } + } + return values; + } + + @Override + public List buildResult(RuleActionContext ctx, Object value) { + List out = new ArrayList<>(); + for (CarryValue v : (List) value) { + if (v.rowNum == null) { + for (String name : resolveWidgetVariableNames(ctx, v.targetField)) { + out.add(buildVo(name, AttributeEnum.VALUE_CHANGE, v.value)); + } + } else { + out.add(buildEntryVo(ctx.getEventWidgetVariableName(), v.targetField, AttributeEnum.VALUE_CHANGE, v.value, v.rowNum)); + } + } + return out; + } + + private static class CarryValue { + final String targetField; + final Object value; + final String rowNum; + CarryValue(String targetField, Object value, String rowNum) { + this.targetField = targetField; + this.value = value; + this.rowNum = rowNum; + } + } +} diff --git a/common/common-online/src/main/java/apelet/common/online/rule/action/impl/FieldAssignExecutor.java b/common/common-online/src/main/java/apelet/common/online/rule/action/impl/FieldAssignExecutor.java new file mode 100644 index 0000000..b1a9ff5 --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/rule/action/impl/FieldAssignExecutor.java @@ -0,0 +1,97 @@ +package apelet.common.online.rule.action.impl; + +import apelet.common.core.object.ObjectCollection; +import apelet.common.core.object.ObjectValue; +import apelet.common.online.model.constant.AttributeEnum; +import apelet.common.online.rule.action.AbstractRuleActionExecutor; +import apelet.common.online.rule.enums.RuleActionTypeEnum; +import apelet.common.online.rule.model.RuleActionContext; +import apelet.common.online.util.Jexl3Util; +import apelet.common.online.vo.OnlineEventPluginExecuteVo; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; + +/** + * type 3 设置字段值:sourceFields 灵活解析(主表 表.字段 / 表.关联对象.子字段,分录取当前编辑行); + * 目标字段归属分录表时写当前编辑行并产分录 Vo,主表目标写主表对象。 + */ +@Component +public class FieldAssignExecutor extends AbstractRuleActionExecutor { + + @Resource + private Jexl3Util jexl3Util; + + @Override + public RuleActionTypeEnum getActionType() { + return RuleActionTypeEnum.FIELD_ASSIGN; + } + + @Override + public Object resolveValue(RuleActionContext ctx) { + List values = new ArrayList<>(); + ObjectValue objectValue = ctx.getObjectValue(); + JSONArray array = ctx.getAction().getSource().getJSONArray("specifyList"); + if (array == null) { + return values; + } + String rowKeySubset = ctx.getDto().getRowKeySubset(); + for (Object o : array) { + JSONObject o2 = (JSONObject) o; + String sourceFields = (String) o2.get("sourceFields"); + String targetFields = (String) o2.get("targetFields"); + String[] sourceParts = sourceFields.split("\\."); + String[] targetParts = targetFields.split("\\."); + String targetField = targetParts[targetParts.length - 1]; + Object v = jexl3Util.resolveRefValue(sourceFields, objectValue, rowKeySubset); + String entryTable = resolveEntryTable(objectValue, targetParts, sourceParts); + ObjectValue row = entryTable == null ? null : jexl3Util.currentRow((ObjectCollection) objectValue.get(entryTable), rowKeySubset, ctx.getDto().getRowKey()); + if (entryTable != null && row == null) { + continue; // 附表目标但表为空,跳过,避免误写主表 + } + if (row != null) { + row.put(targetField, v); // 附表目标:写回当前编辑行 + String tabId = resolveEntryTabId(objectValue, entryTable, ctx.getEventWidgetVariableName()); + if (tabId != null) { + values.add(new AssignValue(targetField, v, tabId, row.getString("rowkey"))); + } + } else { + objectValue.put(targetField, v); // 主表目标:写回表单对象,供后续规则条件求值 + values.add(new AssignValue(targetField, v, null, null)); + } + } + return values; + } + + @Override + public List buildResult(RuleActionContext ctx, Object value) { + List out = new ArrayList<>(); + for (AssignValue v : (List) value) { + if (v.rowNum == null) { + for (String name : resolveWidgetVariableNames(ctx, v.targetField)) { + out.add(buildVo(name, AttributeEnum.VALUE_CHANGE, v.value)); + } + } else { + out.add(buildEntryVo(v.tabId, v.targetField, AttributeEnum.VALUE_CHANGE, v.value, v.rowNum)); + } + } + return out; + } + + private static class AssignValue { + final String targetField; + final Object value; + final String tabId; + final String rowNum; + AssignValue(String targetField, Object value, String tabId, String rowNum) { + this.targetField = targetField; + this.value = value; + this.tabId = tabId; + this.rowNum = rowNum; + } + } +} diff --git a/common/common-online/src/main/java/apelet/common/online/rule/action/impl/FieldClearExecutor.java b/common/common-online/src/main/java/apelet/common/online/rule/action/impl/FieldClearExecutor.java new file mode 100644 index 0000000..12406ac --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/rule/action/impl/FieldClearExecutor.java @@ -0,0 +1,97 @@ +package apelet.common.online.rule.action.impl; + +import apelet.common.core.object.ObjectCollection; +import apelet.common.core.object.ObjectValue; +import apelet.common.online.model.constant.AttributeEnum; +import apelet.common.online.rule.action.AbstractRuleActionExecutor; +import apelet.common.online.rule.enums.RuleActionTypeEnum; +import apelet.common.online.rule.model.RuleActionContext; +import apelet.common.online.util.Jexl3Util; +import apelet.common.online.vo.OnlineEventPluginExecuteVo; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * type 4 清除字段值:主表直接清,分录从主表对象取集合后逐行清(空表跳过,tabId 经 keyNameObj 反查、缺失兜底事件控件)。 + */ +@Component +public class FieldClearExecutor extends AbstractRuleActionExecutor { + + @Resource + private Jexl3Util jexl3Util; + + @Override + public RuleActionTypeEnum getActionType() { + return RuleActionTypeEnum.FIELD_CLEAR; + } + + @Override + public Object resolveValue(RuleActionContext ctx) { + List values = new ArrayList<>(); + ObjectValue objectValue = ctx.getObjectValue(); + JSONArray treeList = ctx.getAction().getSource().getJSONArray("treeList"); + if (treeList == null) { + return values; + } + for (Object o : treeList) { + JSONObject jsonObject = (JSONObject) o; + if (jsonObject.containsKey("zxName")) { + String[] split = jsonObject.getString("zxName").split("\\."); + if (split[0].equals(split[1])) { // 主表字段 + String label = jsonObject.get("label").toString(); + objectValue.put(label, ""); // 写回清空,供后续规则条件求值 + values.add(new ClearValue(label, null, null)); + } else { // 分录字段:主表对象里取分录集合,清当前编辑行 + String tab = jexl3Util.getMySplit("\\((.*?)\\)", split[1]); + ObjectCollection collection = (ObjectCollection) objectValue.get(tab); // 从主表对象取分录集合(新增空分录表时为空) + if (collection == null || collection.isEmpty()) { + continue; // 附表为空,无行可清 + } + ObjectValue current = jexl3Util.currentRow(collection, ctx.getDto().getRowKeySubset(), ctx.getDto().getRowKey()); + if (current == null) { + continue; // 无当前编辑行(表单加载等),跳过不清,避免误清其他行 + } + String label = jsonObject.get("label").toString(); + String tabId = resolveEntryTabId(objectValue, tab, ctx.getEventWidgetVariableName()); + current.put(label, ""); // 只清当前编辑行,避免连带清空其他行 + if (tabId != null) { + values.add(new ClearValue(label, tabId, current.getString("rowkey"))); + } + } + } + } + return values; + } + + @Override + public List buildResult(RuleActionContext ctx, Object value) { + List out = new ArrayList<>(); + for (ClearValue v : (List) value) { + if (v.rowNum == null) { + for (String name : resolveWidgetVariableNames(ctx, v.field)) { + out.add(buildVo(name, AttributeEnum.VALUE_CHANGE, "")); + } + } else { + out.add(buildEntryVo(v.tabId, v.field, AttributeEnum.VALUE_CHANGE, "", v.rowNum)); + } + } + return out; + } + + private static class ClearValue { + final String field; + final String tabId; + final String rowNum; + ClearValue(String field, String tabId, String rowNum) { + this.field = field; + this.tabId = tabId; + this.rowNum = rowNum; + } + } +} diff --git a/common/common-online/src/main/java/apelet/common/online/rule/action/impl/FormulaAssignExecutor.java b/common/common-online/src/main/java/apelet/common/online/rule/action/impl/FormulaAssignExecutor.java new file mode 100644 index 0000000..e324536 --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/rule/action/impl/FormulaAssignExecutor.java @@ -0,0 +1,162 @@ +package apelet.common.online.rule.action.impl; + +import apelet.common.core.object.ObjectCollection; +import apelet.common.core.object.ObjectValue; +import apelet.common.online.model.constant.AttributeEnum; +import apelet.common.online.rule.action.AbstractRuleActionExecutor; +import apelet.common.online.rule.condition.FunctionExecutor; +import apelet.common.online.rule.enums.RuleActionTypeEnum; +import apelet.common.online.rule.model.RuleActionContext; +import apelet.common.online.util.Jexl3Util; +import apelet.common.online.vo.OnlineEventPluginExecuteVo; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.util.*; + +/** + * type 1 计算并赋值:拆 expressionTranslation,公式引用字段→算值、字面量直接赋、$fun$→函数; + * 目标字段归属分录表时写当前编辑行并产分录 Vo,主表目标写主表对象。 + */ +@Component +public class FormulaAssignExecutor extends AbstractRuleActionExecutor { + + @Resource + private Jexl3Util jexl3Util; + @Resource + private FunctionExecutor functionExecutor; + + @Override + public RuleActionTypeEnum getActionType() { + return RuleActionTypeEnum.FORMULA_ASSIGN; + } + + @Override + public Object resolveValue(RuleActionContext ctx) { + String expressionTranslation = ctx.getAction().getSource().getString("expressionTranslation"); + if (expressionTranslation == null || !expressionTranslation.contains("=")) { + return null; + } + String[] split = expressionTranslation.split("="); + String tag = split[0].trim(); + String src = split[1].trim(); + ObjectValue objectValue = ctx.getObjectValue(); + String targetField = lastSegment(tag); + String rowKeySubset = ctx.getDto().getRowKeySubset(); + Object result; + if (src.contains("$")) { // 分支 C:$fun$ 函数赋值 + String fun = jexl3Util.getMySplit("\\$(.*?)\\$", src); + result = functionExecutor.execute(ctx.getFunctionForm(), fun, objectValue); + } else if (!src.contains(objectValue.getTableName())) { // 分支 B:字面量直接赋值 + result = parseLiteral(src); + } else { // 分支 A:公式引用字段,替换为实际值后求值(空值补 0) + Map context = new HashMap<>(); + for (String ref : extractFieldRefs(src)) { + if (ref.contains(objectValue.getTableName()) && !context.containsKey(ref)) { + context.put(ref, resolveFormulaField(ref, objectValue, rowKeySubset)); + } + } + result = jexl3Util.expression(src, context); + } + String[] tagParts = tag.split("\\."); + List srcRefs = extractFieldRefs(src); + String[] srcParts = srcRefs.isEmpty() ? new String[0] : srcRefs.get(0).split("\\."); + String entryTable = resolveEntryTable(objectValue, tagParts, srcParts); + ObjectValue row = entryTable == null ? null : jexl3Util.currentRow((ObjectCollection) objectValue.get(entryTable), rowKeySubset, ctx.getDto().getRowKey()); + if (entryTable != null && row == null) { + return null; // 附表目标但表为空,跳过,避免误写主表 + } + if (row != null) { + row.put(targetField, result); // 附表目标:写回当前编辑行 + String tabId = resolveEntryTabId(objectValue, entryTable, ctx.getEventWidgetVariableName()); + return tabId == null ? null : new FormulaValue(targetField, result, tabId, row.getString("rowkey")); + } + objectValue.put(targetField, result); // 主表目标:写回表单对象,供后续规则条件求值 + return new FormulaValue(targetField, result, null, null); + } + + @Override + public List buildResult(RuleActionContext ctx, Object value) { + FormulaValue fv = (FormulaValue) value; + if (fv == null) { + return Collections.emptyList(); + } + List out = new ArrayList<>(); + if (fv.rowNum == null) { + for (String name : resolveWidgetVariableNames(ctx, fv.targetField)) { + out.add(buildVo(name, AttributeEnum.VALUE_CHANGE, fv.value)); + } + } else { + out.add(buildEntryVo(fv.tabId, fv.targetField, AttributeEnum.VALUE_CHANGE, fv.value, fv.rowNum)); + } + return out; + } + + private String lastSegment(String tag) { + String[] parts = tag.split("\\."); + return parts[parts.length - 1].trim(); + } + + /** 解析公式中 表.表.字段 / 表.字段 引用的实际值:主表直接取/下钻关联对象,分录取当前编辑行;空值补 0,数字字符串转数值(避免字符串相加) */ + private Object resolveFormulaField(String ref, ObjectValue objectValue, String rowKeySubset) { + String[] parts = ref.split("\\."); + Object value; + Object first = objectValue.get(parts[1]); + if (!parts[0].equals(parts[1]) && first instanceof ObjectCollection) { // 分录:表.分录表.字段 + ObjectValue row = jexl3Util.currentRow((ObjectCollection) first, rowKeySubset, (String) objectValue.get("rowKey")); + value = row == null ? null : row.get(parts[2]); + } else { // 主表:表.字段 / 表.表.字段 / 表.关联对象.子字段 + value = jexl3Util.resolveFieldChain(objectValue, parts, parts[0].equals(parts[1]) ? 2 : 1); + } + if (value == null || value.toString().isEmpty()) { + return 0; + } + return value instanceof String ? toNumber((String) value) : value; + } + + /** 解析字面量赋值:去首尾引号,'' → 空串,数字字符串转数值,其余原样返回 */ + private Object parseLiteral(String src) { + String trim = src.trim(); + if (trim.length() >= 2 + && ((trim.startsWith("'") && trim.endsWith("'")) || (trim.startsWith("\"") && trim.endsWith("\"")))) { + trim = trim.substring(1, trim.length() - 1); + } + return toNumber(trim); + } + + /** 数字字符串转数值:正则判数字 → 整数转 Long,小数转 BigDecimal;非数字原样返回 */ + private Object toNumber(String s) { + String trim = s.trim(); + if (!trim.matches("-?\\d+(\\.\\d+)?")) { + return s; + } + if (trim.contains(".")) { + return new BigDecimal(trim); + } + return Long.valueOf(trim); + } + + private List extractFieldRefs(String src) { + List refs = new ArrayList<>(); + for (String token : src.split("[^a-zA-Z0-9_.]+")) { + if (!token.isEmpty() && token.split("\\.").length >= 2 && !refs.contains(token)) { + refs.add(token); + } + } + return refs; + } + + private static class FormulaValue { + final String targetField; + final Object value; + final String tabId; + final String rowNum; + FormulaValue(String targetField, Object value, String tabId, String rowNum) { + this.targetField = targetField; + this.value = value; + this.tabId = tabId; + this.rowNum = rowNum; + } + } +} diff --git a/common/common-online/src/main/java/apelet/common/online/rule/action/impl/WidgetHideExecutor.java b/common/common-online/src/main/java/apelet/common/online/rule/action/impl/WidgetHideExecutor.java new file mode 100644 index 0000000..cc48c32 --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/rule/action/impl/WidgetHideExecutor.java @@ -0,0 +1,27 @@ +package apelet.common.online.rule.action.impl; + +import apelet.common.online.model.constant.AttributeEnum; +import apelet.common.online.rule.action.AbstractRuleActionExecutor; +import apelet.common.online.rule.enums.RuleActionTypeEnum; +import apelet.common.online.rule.model.RuleActionContext; +import apelet.common.online.vo.OnlineEventPluginExecuteVo; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * type 8 隐藏控件 → SHOW=false。 + */ +@Component +public class WidgetHideExecutor extends AbstractRuleActionExecutor { + + @Override + public RuleActionTypeEnum getActionType() { + return RuleActionTypeEnum.WIDGET_HIDE; + } + + @Override + public List buildResult(RuleActionContext ctx, Object value) { + return buildFromTreeList(ctx, AttributeEnum.SHOW, false); + } +} diff --git a/common/common-online/src/main/java/apelet/common/online/rule/action/impl/WidgetLockExecutor.java b/common/common-online/src/main/java/apelet/common/online/rule/action/impl/WidgetLockExecutor.java new file mode 100644 index 0000000..2f24007 --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/rule/action/impl/WidgetLockExecutor.java @@ -0,0 +1,51 @@ +package apelet.common.online.rule.action.impl; + +import apelet.common.online.model.constant.AttributeEnum; +import apelet.common.online.rule.action.AbstractRuleActionExecutor; +import apelet.common.online.rule.enums.RuleActionTypeEnum; +import apelet.common.online.rule.model.RuleActionContext; +import apelet.common.online.vo.OnlineEventPluginExecuteVo; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * type 5 锁定控件:普通控件 → DISABLED;btnId 特例(锁父容器内按钮)→ CHANGEOPERATE 数组。 + */ +@Component +public class WidgetLockExecutor extends AbstractRuleActionExecutor { + + @Override + public RuleActionTypeEnum getActionType() { + return RuleActionTypeEnum.WIDGET_LOCK; + } + + @Override + public List buildResult(RuleActionContext ctx, Object value) { + List out = new ArrayList<>(); + JSONArray treeList = ctx.getAction().getSource().getJSONArray("treeList"); + if (treeList == null) { + return out; + } + for (Object o : treeList) { + JSONObject jsonObject = (JSONObject) o; + String name = jsonObject.get("variableName").toString(); + if (jsonObject.containsKey("btnId")) { // 特例:锁定父容器内按钮 + String showName = jsonObject.get("showName").toString(); + String parentVariableName = jsonObject.get("parentVariableName").toString(); + JSONArray objects = new JSONArray(); + JSONObject json = new JSONObject(); + json.put("name", showName); + json.put("disable", true); + objects.add(json); + out.add(buildVo(parentVariableName, AttributeEnum.CHANGEOPERATE, objects)); + } else { // 普通控件锁定 + out.add(buildVo(name, AttributeEnum.DISABLED, true)); + } + } + return out; + } +} diff --git a/common/common-online/src/main/java/apelet/common/online/rule/action/impl/WidgetShowExecutor.java b/common/common-online/src/main/java/apelet/common/online/rule/action/impl/WidgetShowExecutor.java new file mode 100644 index 0000000..e3a8c0a --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/rule/action/impl/WidgetShowExecutor.java @@ -0,0 +1,27 @@ +package apelet.common.online.rule.action.impl; + +import apelet.common.online.model.constant.AttributeEnum; +import apelet.common.online.rule.action.AbstractRuleActionExecutor; +import apelet.common.online.rule.enums.RuleActionTypeEnum; +import apelet.common.online.rule.model.RuleActionContext; +import apelet.common.online.vo.OnlineEventPluginExecuteVo; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * type 7 显示控件 → SHOW=true。 + */ +@Component +public class WidgetShowExecutor extends AbstractRuleActionExecutor { + + @Override + public RuleActionTypeEnum getActionType() { + return RuleActionTypeEnum.WIDGET_SHOW; + } + + @Override + public List buildResult(RuleActionContext ctx, Object value) { + return buildFromTreeList(ctx, AttributeEnum.SHOW, true); + } +} diff --git a/common/common-online/src/main/java/apelet/common/online/rule/action/impl/WidgetUnlockExecutor.java b/common/common-online/src/main/java/apelet/common/online/rule/action/impl/WidgetUnlockExecutor.java new file mode 100644 index 0000000..49a5843 --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/rule/action/impl/WidgetUnlockExecutor.java @@ -0,0 +1,27 @@ +package apelet.common.online.rule.action.impl; + +import apelet.common.online.model.constant.AttributeEnum; +import apelet.common.online.rule.action.AbstractRuleActionExecutor; +import apelet.common.online.rule.enums.RuleActionTypeEnum; +import apelet.common.online.rule.model.RuleActionContext; +import apelet.common.online.vo.OnlineEventPluginExecuteVo; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * type 6 解锁控件 → DISABLED=false。 + */ +@Component +public class WidgetUnlockExecutor extends AbstractRuleActionExecutor { + + @Override + public RuleActionTypeEnum getActionType() { + return RuleActionTypeEnum.WIDGET_UNLOCK; + } + + @Override + public List buildResult(RuleActionContext ctx, Object value) { + return buildFromTreeList(ctx, AttributeEnum.DISABLED, false); + } +} diff --git a/common/common-online/src/main/java/apelet/common/online/rule/condition/ConditionEvaluator.java b/common/common-online/src/main/java/apelet/common/online/rule/condition/ConditionEvaluator.java new file mode 100644 index 0000000..d62ecec --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/rule/condition/ConditionEvaluator.java @@ -0,0 +1,121 @@ +package apelet.common.online.rule.condition; + +import apelet.common.core.object.ObjectCollection; +import apelet.common.core.object.ObjectValue; +import apelet.common.online.rule.model.RuleCondition; +import apelet.common.online.rule.model.RuleEvalContext; +import apelet.common.online.util.Jexl3Util; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 规则级条件求值:一条规则只求值一次,用结果选 condition=1/2 的动作。 + * 加载表达式里【所有】表.表.字段 / 表.字段 token 进 context(修复:旧 getExpressionValue 只取第一个字段)。 + */ +@Component +public class ConditionEvaluator { + + private static final Pattern FIELD_REF = Pattern.compile("[a-zA-Z0-9_]+(\\.[a-zA-Z0-9_]+)+"); + + @Resource + private Jexl3Util jexl3Util; + @Resource + private FunctionExecutor functionExecutor; + + public boolean evaluate(RuleCondition condition, RuleEvalContext ctx) { + String expression = condition.getExpression(); + if (StringUtils.isEmpty(expression)) return false; + boolean result = true; + for (String expr : expression.split("&&")) { + expr = expr.trim(); + if (expr.isEmpty()) continue; + if (expr.startsWith("$")) { + String fun = jexl3Util.getMySplit("\\$(.*?)\\$", expr); + Object value = functionExecutor.execute(condition.getFunctionForm(), fun, ctx.getObjectValue()); + result = jexl3Util.judgmentFunc(expr, value) && result; + } else { + result = evaluateSimple(expr, ctx.getObjectValue()) && result; + } + } + return result; + } + + private boolean evaluateSimple(String expr, ObjectValue objectValue) { + List refs = extractFieldRefs(expr); + Map context = new HashMap<>(); + String entryTable = null; + for (String ref : refs) { + String[] parts = ref.split("\\."); + if (parts.length < 2) continue; + // 分录:起始段在 objectValue 里是分录集合;其余都是主表字段(兼容 表.表.字段 / 表.字段 两种格式) + if (!parts[0].equals(parts[1]) && objectValue.get(parts[1]) instanceof ObjectCollection) { + if (entryTable == null) entryTable = parts[1]; + } else { + context.put(ref, resolveMasterField(ref, objectValue)); + } + } + if (entryTable == null) { + Object val = jexl3Util.expression(expr, context); + return val instanceof Boolean && (Boolean) val; + } + return evaluateEntryRows(expr, refs, entryTable, context, objectValue); + } + + /** 分录表达式:命中当前编辑行逐行求值(rowKeySubset 行内编辑 / rowKey 分录编辑完成 / 无过滤取新增行),任一行为 true 即成立 */ + private boolean evaluateEntryRows(String expr, List refs, String entryTable, + Map baseContext, ObjectValue objectValue) { + ObjectCollection rows = (ObjectCollection) objectValue.get(entryTable); + if (rows == null || rows.isEmpty()) return false; + String rowKeySubset = (String) objectValue.get("rowKeySubset"); + String rowKey = (String) objectValue.get("rowKey"); + Set targetSet = new HashSet<>(); + if (StringUtils.isNotEmpty(rowKeySubset)) Collections.addAll(targetSet, rowKeySubset.split(",")); + Iterator iterator = rows.iterator(); + while (iterator.hasNext()) { + ObjectValue row = (ObjectValue) iterator.next(); + if (row.getValues() == null || row.getValues().isEmpty()) continue; + String k = row.getString("rowkey"); + if (StringUtils.isNotEmpty(rowKeySubset)) { // 行内编辑:命中 subset 的行 + if (k == null || !targetSet.contains(k)) continue; + } else if (StringUtils.isNotEmpty(rowKey)) { // 分录编辑完成:命中当前行 rowKey + if (!rowKey.equals(k)) continue; + } else { // 无过滤:跳过已保存行,只评估新增行 + if (k != null) continue; + } + Map context = new HashMap<>(baseContext); + for (String ref : refs) { + String[] parts = ref.split("\\."); + if (parts.length >= 2 && entryTable.equals(parts[1])) { + context.put(ref, resolveRowField(row, parts)); + } + } + Object val = jexl3Util.expression(expr, context); + if (val instanceof Boolean && (Boolean) val) return true; + } + return false; + } + + private Object resolveMasterField(String ref, ObjectValue objectValue) { + String[] parts = ref.split("\\."); + return jexl3Util.resolveFieldChain(objectValue, parts, parts[0].equals(parts[1]) ? 2 : 1); + } + + private Object resolveRowField(ObjectValue row, String[] parts) { + return jexl3Util.resolveFieldChain(row, parts, 2); + } + + private List extractFieldRefs(String expr) { + List refs = new ArrayList<>(); + Matcher matcher = FIELD_REF.matcher(expr); + while (matcher.find()) { + String ref = matcher.group(); + if (!refs.contains(ref)) refs.add(ref); + } + return refs; + } +} diff --git a/common/common-online/src/main/java/apelet/common/online/rule/condition/FunctionExecutor.java b/common/common-online/src/main/java/apelet/common/online/rule/condition/FunctionExecutor.java new file mode 100644 index 0000000..e38381b --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/rule/condition/FunctionExecutor.java @@ -0,0 +1,67 @@ +package apelet.common.online.rule.condition; + +import apelet.common.core.exception.MyRuntimeException; +import apelet.common.core.object.ObjectValue; +import apelet.common.online.model.SysFunction; +import apelet.common.online.service.ISysFunctionService; +import apelet.common.online.util.Jexl3Util; +import apelet.common.online.vo.SysFunctionVo; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * 封装 $fun$ 动态编译函数调用(javassist),条件求值与 type1 动作赋值共用。 + */ +@Component +public class FunctionExecutor { + + @Resource + private ISysFunctionService sysFunctionService; + @Resource + private Jexl3Util jexl3Util; + + /** 规则引擎路径:提取函数参数后执行 */ + public Object execute(Map functionForm, String fun, ObjectValue objectValue) { + List funParam = Arrays.asList(jexl3Util.getMySplit("\\((.*?)\\)", fun).split(",")); + Object[] objects = jexl3Util.getFunParam(funParam, objectValue); + return executeWithParams(functionForm, fun, objects); + } + + /** 核心调用:给定已提取的参数,查函数并动态编译执行(Save/Audit 插件经 OnlineFormServiceImpl.executeFunction 也走这里) */ + public Object executeWithParams(Map functionForm, String fun, Object[] objects) { + if (fun == null) { + return null; + } + String funName = fun.substring(0, fun.indexOf('(')).trim(); + SysFunction sysFunction = resolveSysFunction(functionForm, funName); + SysFunctionVo sysFunctionVo = new SysFunctionVo(); + BeanUtils.copyProperties(sysFunction, sysFunctionVo); + sysFunctionVo.setParameter(objects); + return sysFunctionService.onLineFunction(sysFunctionVo); + } + + /** + * 解析 $fun$ 引用的函数:functionForm 映射的 ID 优先 getById;ID 缺失/查不到时按函数名或编码(function_name/number)查询 + * (兼容 functionName 字段后续改为 number 字段);名称或编码匹配到 0 个或多个视为配置错误,直接失效。 + */ + private SysFunction resolveSysFunction(Map functionForm, String funName) { + Long funId = functionForm == null ? null : functionForm.get(funName); + if (funId != null) { + SysFunction f = sysFunctionService.getById(funId); + if (f != null) return f; + } + List list = sysFunctionService.list(new LambdaQueryWrapper() + .eq(SysFunction::getFunctionName, funName) + .or().eq(SysFunction::getNumber, funName)); + if (list == null || list.size() != 1) { + throw new MyRuntimeException("函数 [" + funName + "] 不存在或匹配到多个,无法执行 $fun$"); + } + return list.get(0); + } +} diff --git a/common/common-online/src/main/java/apelet/common/online/rule/enums/EventTypeEnum.java b/common/common-online/src/main/java/apelet/common/online/rule/enums/EventTypeEnum.java new file mode 100644 index 0000000..c0a9aee --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/rule/enums/EventTypeEnum.java @@ -0,0 +1,26 @@ +package apelet.common.online.rule.enums; + +import lombok.Getter; + +/** + * 归一化后的事件类型,由动作 checkList 归一化而来: + * "加载" → FORM_CREATED;"值更新"/"值改变" → CHANGE;空数组 → ALL(监听全部事件)。 + * + * 触发匹配:eventCode = formCreated 命中 FORM_CREATED,其余命中 CHANGE;ALL 恒命中。 + */ +public enum EventTypeEnum { + + /** 全部事件:checkList 为空数组,动作在所有事件下都触发(修复空数组「永不执行」行为反转点) */ + ALL("全部事件(checkList 为空)"), + /** 页面加载:checkList 含「加载」,对应 eventCode = formCreated */ + FORM_CREATED("页面加载(checkList 含「加载」,formCreated)"), + /** 值改变:checkList 含「值更新」/「值改变」,对应 eventCode = change */ + CHANGE("值改变(checkList 含「值更新」/「值改变」,change)"); + + @Getter + private final String description; + + EventTypeEnum(String description) { + this.description = description; + } +} diff --git a/common/common-online/src/main/java/apelet/common/online/rule/enums/RuleActionTypeEnum.java b/common/common-online/src/main/java/apelet/common/online/rule/enums/RuleActionTypeEnum.java new file mode 100644 index 0000000..51218d8 --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/rule/enums/RuleActionTypeEnum.java @@ -0,0 +1,44 @@ +package apelet.common.online.rule.enums; + +import lombok.Getter; + +import java.util.Arrays; + +/** + * 规则动作类型(1~8),对应规则 JSON allList[].type,Registry 按它路由到 RuleActionExecutor 实现。 + * + * 1~4 为数据类动作(业务规则 businessRulesList),5~8 为控件状态类动作(界面规则 conditionTypeList): + *
+ * 1 计算并赋值   → FormulaAssignExecutor   金额 = 数量 * 单价
+ * 2 携带基础资料 → BaseDataCarryExecutor   创建人.fname → 提交人
+ * 3 设置字段值   → FieldAssignExecutor     源字段值 → 目标字段
+ * 4 清除字段值   → FieldClearExecutor      清空字段
+ * 5~8 锁定/解锁/显示/隐藏控件(WidgetLock/Unlock/Show/HideExecutor)
+ * 
+ */ +@Getter +public enum RuleActionTypeEnum { + + FORMULA_ASSIGN(1, "计算并赋值"), + BASE_DATA_CARRY(2, "携带基础资料"), + FIELD_ASSIGN(3, "设置字段值"), + FIELD_CLEAR(4, "清除字段值"), + WIDGET_LOCK(5, "锁定控件"), + WIDGET_UNLOCK(6, "解锁控件"), + WIDGET_SHOW(7, "显示控件"), + WIDGET_HIDE(8, "隐藏控件"); + + private final int code; + private final String description; + + RuleActionTypeEnum(int code, String description) { + this.code = code; + this.description = description; + } + + /** 由 widgetJson 动作里的 type 值(1~8)转枚举,未识别抛异常 */ + public static RuleActionTypeEnum of(int code) { + return Arrays.stream(values()).filter(t -> t.code == code).findFirst() + .orElseThrow(() -> new IllegalArgumentException("未识别的动作类型: " + code)); + } +} diff --git a/common/common-online/src/main/java/apelet/common/online/rule/enums/RuleTypeEnum.java b/common/common-online/src/main/java/apelet/common/online/rule/enums/RuleTypeEnum.java new file mode 100644 index 0000000..0d4dc2a --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/rule/enums/RuleTypeEnum.java @@ -0,0 +1,23 @@ +package apelet.common.online.rule.enums; + +import lombok.Getter; + +/** + * 规则来源类型:界面规则 / 业务规则。 + * 对应 widgetJson 两个数组:conditionTypeList(界面规则) / businessRulesList(业务规则)。 + * 两个数组在代码里一视同仁处理(同一管线),ruleType 仅作来源标记,供前端差异化展示或排查。 + */ +public enum RuleTypeEnum { + + /** 界面规则:widgetJson 的 conditionTypeList 数组(锁定/解锁/显隐等控件状态类动作) */ + INTERFACE("界面规则(conditionTypeList)"), + /** 业务规则:widgetJson 的 businessRulesList 数组(赋值/携带基础资料等数据类动作) */ + BUSINESS("业务规则(businessRulesList)"); + + @Getter + private final String description; + + RuleTypeEnum(String description) { + this.description = description; + } +} diff --git a/common/common-online/src/main/java/apelet/common/online/rule/model/FormRule.java b/common/common-online/src/main/java/apelet/common/online/rule/model/FormRule.java new file mode 100644 index 0000000..f2f4c55 --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/rule/model/FormRule.java @@ -0,0 +1,32 @@ +package apelet.common.online.rule.model; + +import apelet.common.online.rule.enums.RuleTypeEnum; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 规则定义:一条规则 = 一个条件 + 一组动作。 + * + * 对应 widgetJson 里 businessRulesList / conditionTypeList 数组的【一个元素】: + *
+ * {
+ *   "Enable": true,                                    // → enable
+ *   "expressionTranslation": "t_ruletest.t_ruletest.qty NOT NULL && t_ruletest.t_ruletest.prices NOT NULL",  // → condition
+ *   "allList": [ { "type": 1, "condition": "1", ... } ] // → actions
+ * }
+ * 
+ * 求值流程:条件成立(establish=true) → 执行 condition=1 的动作;不成立 → 执行 condition=2 的动作。 + */ +@Data +public class FormRule { + private RuleTypeEnum ruleType; + private boolean enable = true; + private RuleCondition condition; + private List actions = new ArrayList<>(); + + public void addAction(RuleAction action) { + actions.add(action); + } +} diff --git a/common/common-online/src/main/java/apelet/common/online/rule/model/RuleAction.java b/common/common-online/src/main/java/apelet/common/online/rule/model/RuleAction.java new file mode 100644 index 0000000..9679be3 --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/rule/model/RuleAction.java @@ -0,0 +1,45 @@ +package apelet.common.online.rule.model; + +import apelet.common.online.rule.enums.EventTypeEnum; +import apelet.common.online.rule.enums.RuleActionTypeEnum; +import com.alibaba.fastjson.JSONObject; +import lombok.Data; + +import java.util.List; + +/** + * 规则动作:一个动作类型一个执行器。 + * 对应规则 JSON allList 数组里的【一个元素】。 + * + * 示例(type 1 计算赋值): + *
+ * {
+ *   "type": 1,                              // → actionType(FormulaAssignExecutor 处理)
+ *   "condition": "1",                       // → condition(1=条件成立时执行,2=不成立时执行)
+ *   "checkList": ["加载", "值更新"],          // → eventTypes 归一化
+ *   "expressionTranslation": "t_ruletest.t_ruletest.amount = t_ruletest.t_ruletest.qty * t_ruletest.t_ruletest.prices"
+ * }
+ * 
+ */ +@Data +public class RuleAction { + /** 动作类型(1~8),Registry 靠它路由到对应执行器 */ + private RuleActionTypeEnum actionType; + /** 1=表达式成立时执行,2=不成立时执行 */ + private int condition; + /** 执行顺序(规则内动作按 sort 升序执行,缺失按 0) */ + private int sort; + /** checkList 归一化后的事件("加载"→FORM_CREATED,"值更新"/"值改变"→CHANGE,空→ALL) */ + private List eventTypeEnums; + /** 原始动作 JSON(各 type 结构差异大,执行器按需取) */ + private JSONObject source; + + /** 事件匹配:eventTypes 含 ALL 或含当前事件归一化结果则匹配 */ + public boolean matchEvent(String eventCode) { + if (eventTypeEnums == null || eventTypeEnums.contains(EventTypeEnum.ALL)) { + return true; + } + EventTypeEnum normalized = "formCreated".equals(eventCode) ? EventTypeEnum.FORM_CREATED : EventTypeEnum.CHANGE; + return eventTypeEnums.contains(normalized); + } +} diff --git a/common/common-online/src/main/java/apelet/common/online/rule/model/RuleActionContext.java b/common/common-online/src/main/java/apelet/common/online/rule/model/RuleActionContext.java new file mode 100644 index 0000000..9322902 --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/rule/model/RuleActionContext.java @@ -0,0 +1,31 @@ +package apelet.common.online.rule.model; + +import apelet.common.core.object.ObjectValue; +import apelet.common.online.dto.OnlineEventPluginExecuteDto; +import apelet.common.online.model.OnlineForm; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Map; + +/** + * 执行上下文(执行器用):收敛执行器要访问的数据,由 FormRuleEngine.executeRule 组装后交给执行器。 + */ +@Getter +@AllArgsConstructor +public class RuleActionContext { + /** 当前表单对象:buildResult 从 form.getWidgetJson() 反查列名 → 控件 variableName(executeAction 无表单时为空) */ + private final OnlineForm form; + /** 当前表单数据对象:主表字段 + 分录集合,执行器用 Jexl3Util.resolveFieldChain 从它下钻关联表 */ + private final ObjectValue objectValue; + /** 事件插件执行 dto:type2/3 经 getRowKeySubset() 取当前编辑行(executeAction 通用入口为空对象) */ + private final OnlineEventPluginExecuteDto dto; + /** 当前动作:执行器从 action.getSource() 取动作参数(specifyList / specifyJiList / expressionTranslation 等) */ + private final RuleAction action; + /** 触发事件编码(executeAction 通用入口为 null,不过滤事件) */ + private final String eventCode; + /** 触发事件的控件名(分录行指令 buildEntryVo 需要) */ + private final String eventWidgetVariableName; + /** 规则级 $函数名$→函数ID 映射(type1 $fun$ 赋值用) */ + private final Map functionForm; +} diff --git a/common/common-online/src/main/java/apelet/common/online/rule/model/RuleCondition.java b/common/common-online/src/main/java/apelet/common/online/rule/model/RuleCondition.java new file mode 100644 index 0000000..c86ffec --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/rule/model/RuleCondition.java @@ -0,0 +1,29 @@ +package apelet.common.online.rule.model; + +import lombok.Data; + +import java.util.List; +import java.util.Map; + +/** + * 规则条件:规则级求值用的表达式(每条规则只求值一次,结果决定走 condition=1 还是 2 的动作)。 + * 对应规则 JSON 顶层的 expressionTranslation / nameList / functionForm。 + * + * 示例: + *
+ * expression   = "t_ruletest.t_ruletest.qty NOT NULL && t_ruletest.t_ruletest.prices NOT NULL"
+ * nameList     = {"t_ruletest": ["qty", "prices"]}
+ * functionForm = {"myFun": 123}        // 表达式里的 $myFun$ 占位 → 函数ID
+ * 
+ * + * 说明:nameList / functionForm 仅为兼容旧 JSON 保留,求值不依赖(ConditionEvaluator 直接扫表达式里的字段引用)。 + */ +@Data +public class RuleCondition { + /** 求值用表达式,真实表名.表名.字段(主表)/ 主表.从表.字段(分录),支持 NOT NULL / == / && 等 */ + private String expression; + /** 表达式涉及的字段映射 {"table":["field"]}(保留兼容,求值不读) */ + private Map> nameList; + /** $函数名$ → 函数ID 映射(保留兼容,函数求值走 FunctionExecutor) */ + private Map functionForm; +} diff --git a/common/common-online/src/main/java/apelet/common/online/rule/model/RuleEvalContext.java b/common/common-online/src/main/java/apelet/common/online/rule/model/RuleEvalContext.java new file mode 100644 index 0000000..5cb6c1f --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/rule/model/RuleEvalContext.java @@ -0,0 +1,19 @@ +package apelet.common.online.rule.model; + +import apelet.common.core.object.ObjectValue; +import apelet.common.online.dto.OnlineEventPluginExecuteDto; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 求值上下文(规则级条件求值用,ConditionEvaluator.evaluate 入口)。 + * 当前求值只读 objectValue;dto 暂未参与求值(分录行的 rowKeySubset 也是从 objectValue 取的)。 + */ +@Getter +@AllArgsConstructor +public class RuleEvalContext { + /** 当前表单数据对象(条件表达式里的字段引用从它解析取值) */ + private final ObjectValue objectValue; + /** 预留:暂未参与条件求值 */ + private final OnlineEventPluginExecuteDto dto; +} diff --git a/common/common-online/src/main/java/apelet/common/online/rule/parser/FormRuleParser.java b/common/common-online/src/main/java/apelet/common/online/rule/parser/FormRuleParser.java new file mode 100644 index 0000000..132de3d --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/rule/parser/FormRuleParser.java @@ -0,0 +1,110 @@ +package apelet.common.online.rule.parser; + +import apelet.common.online.rule.enums.EventTypeEnum; +import apelet.common.online.rule.enums.RuleActionTypeEnum; +import apelet.common.online.rule.enums.RuleTypeEnum; +import apelet.common.online.rule.model.FormRule; +import apelet.common.online.rule.model.RuleAction; +import apelet.common.online.rule.model.RuleCondition; +import apelet.common.online.util.WidgetJsonUtil; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 解析 widgetJson 中 conditionTypeList / businessRulesList 两数组 → List<FormRule>(兼容旧 JSON)。 + */ +@Component +public class FormRuleParser { + + public List parse(String widgetJson) { + List rules = new ArrayList<>(); + if (widgetJson == null) return rules; + rules.addAll(parseArray(WidgetJsonUtil.getRuleArray(widgetJson, "businessRulesList"), RuleTypeEnum.BUSINESS)); + rules.addAll(parseArray(WidgetJsonUtil.getRuleArray(widgetJson, "conditionTypeList"), RuleTypeEnum.INTERFACE)); + return rules; + } + + private List parseArray(JSONArray array, RuleTypeEnum ruleType) { + List rules = new ArrayList<>(); + if (array == null) return rules; + for (Object o : array) { + FormRule rule = parseSingle((JSONObject) o, ruleType); + if (rule != null) rules.add(rule); + } + return rules; + } + + /** 解析单条规则 JSON(businessRulesList / conditionTypeList 数组元素)→ FormRule;Enable=false 或 JSON 为空返回 null */ + public FormRule parseSingle(JSONObject json, RuleTypeEnum ruleType) { + if (json == null || "false".equals(json.getString("Enable"))) return null; + FormRule rule = new FormRule(); + rule.setRuleType(ruleType); + rule.setCondition(new RuleCondition()); + rule.getCondition().setExpression(json.getString("expressionTranslation")); + rule.getCondition().setNameList(parseNameList(json.getJSONObject("nameList"))); + rule.getCondition().setFunctionForm(parseFunctionForm(json.getJSONArray("functionForm"))); + JSONArray allList = json.getJSONArray("allList"); + if (allList != null) { + for (Object a : allList) { + JSONObject aj = (JSONObject) a; + if (aj == null) continue; + RuleAction action = new RuleAction(); + action.setActionType(RuleActionTypeEnum.of(aj.getIntValue("type"))); + action.setCondition(aj.getIntValue("condition")); + action.setSort(aj.getIntValue("sort")); + action.setEventTypeEnums(normalizeEvent(aj.getJSONArray("checkList"))); + action.setSource(aj); + rule.addAction(action); + } + } + return rule; + } + + /** nameList: {"table":["field"]} → Map */ + private Map> parseNameList(JSONObject nameList) { + if (nameList == null) return null; + Map> map = new HashMap<>(); + for (String key : nameList.keySet()) { + map.put(key, nameList.getJSONArray(key).toJavaList(String.class)); + } + return map; + } + + /** functionForm: [{"funName":id,"str":模板}] → Map<funName,id>(跳过 str 等非数字键) */ + private Map parseFunctionForm(JSONArray functionForm) { + if (functionForm == null) return null; + Map map = new HashMap<>(); + for (Object o : functionForm) { + JSONObject json = (JSONObject) o; + for (String key : json.keySet()) { + String value = json.getString(key); + if (StringUtils.isNumeric(value)) { + map.put(key, Long.parseLong(value)); + } + } + } + return map; + } + + /** checkList 归一化:空 → ALL;"加载" → FORM_CREATED;"值更新"/"值改变" → CHANGE */ + private List normalizeEvent(JSONArray checkList) { + List events = new ArrayList<>(); + if (checkList == null || checkList.isEmpty()) { + events.add(EventTypeEnum.ALL); + return events; + } + for (Object o : checkList) { + String event = String.valueOf(o); + if ("加载".equals(event)) events.add(EventTypeEnum.FORM_CREATED); + else events.add(EventTypeEnum.CHANGE); + } + return events; + } +} diff --git a/common/common-online/src/main/java/apelet/common/online/service/IOnlineFormFieldSyncService.java b/common/common-online/src/main/java/apelet/common/online/service/IOnlineFormFieldSyncService.java new file mode 100644 index 0000000..917b3ca --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/service/IOnlineFormFieldSyncService.java @@ -0,0 +1,21 @@ +package apelet.common.online.service; + +/** + * 自动同步字段到历史数据。 + */ +public interface IOnlineFormFieldSyncService { + + /** + * 给所有缺少该字段的 onl_form_head 表补充字段(可按表类型过滤)。 + * 已同步的表(status=1):补 onl_form_field + syncDB 加物理列 + 回写 zz_online_column; + * 未同步的表:只补 onl_form_field,不同步 DB。 + * + * @param fieldName 字段名 + * @param type 字段类型(数据库类型名如 varchar/bigint/decimal,或 onl_fieldType 字典码 0~5) + * @param length 字段长度(varchar 默认 255;decimal 为精度、小数位默认 2) + * @param description 字段描述 + * @param tableType 表类型(0 附表 / 1 主表 / 2 单表,不传则全部) + * @return 同步结果统计 + */ + String syncFieldToHistory(String fieldName, String type, Integer length, String description, String tableType); +} diff --git a/common/common-online/src/main/java/apelet/common/online/service/impl/OnlineFormFieldSyncServiceImpl.java b/common/common-online/src/main/java/apelet/common/online/service/impl/OnlineFormFieldSyncServiceImpl.java new file mode 100644 index 0000000..b7b0d57 --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/service/impl/OnlineFormFieldSyncServiceImpl.java @@ -0,0 +1,208 @@ +package apelet.common.online.service.impl; + +import apelet.common.core.exception.MyRuntimeException; +import apelet.common.dbutil.object.SqlTableColumn; +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.generator.utils.CustomUtil; +import apelet.common.online.model.OnlineDatasource; +import apelet.common.online.model.OnlineDblink; +import apelet.common.online.model.OnlineTable; +import apelet.common.online.service.IOnlineFormFieldSyncService; +import apelet.common.online.service.OnlineColumnService; +import apelet.common.online.service.OnlineDblinkService; +import apelet.common.online.service.OnlineDatasourceService; +import apelet.common.online.service.OnlineTableService; +import apelet.common.sequence.wrapper.IdGeneratorWrapper; +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.github.pagehelper.Page; +import com.github.pagehelper.page.PageMethod; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 自动同步字段到历史数据:给缺字段的 onl_form_head 表补字段。 + */ +@Slf4j +@Service +public class OnlineFormFieldSyncServiceImpl implements IOnlineFormFieldSyncService { + + /** 数据库类型名 → onl_fieldType 字典码 */ + private static final Map TYPE_CODE_MAP = new HashMap<>(); + + static { + TYPE_CODE_MAP.put("varchar", CustomUtil.FieldType_varchar); + TYPE_CODE_MAP.put("nvarchar", CustomUtil.FieldType_varchar); + TYPE_CODE_MAP.put("int", CustomUtil.FieldType_int); + TYPE_CODE_MAP.put("integer", CustomUtil.FieldType_int); + TYPE_CODE_MAP.put("bigint", CustomUtil.FieldType_bigint); + TYPE_CODE_MAP.put("datetime", CustomUtil.FieldType_datetime); + TYPE_CODE_MAP.put("date", CustomUtil.FieldType_datetime); + TYPE_CODE_MAP.put("timestamp", CustomUtil.FieldType_timestamp); + TYPE_CODE_MAP.put("decimal", CustomUtil.FieldType_decimal); + } + + @Autowired + private IOnlFormHeadService onlFormHeadService; + @Autowired + private IOnlFormFieldService onlFormFieldService; + @Autowired + private OnlineTableService onlineTableService; + @Autowired + private OnlineColumnService onlineColumnService; + @Autowired + private OnlineDatasourceService onlineDatasourceService; + @Autowired + private OnlineDblinkService onlineDblinkService; + @Autowired + private IdGeneratorWrapper idGenerator; + + @Override + public String syncFieldToHistory(String fieldName, String type, Integer length, String description, String tableType) { + if (StringUtils.isBlank(fieldName)) { + throw new MyRuntimeException("字段名不能为空"); + } + String fieldType = resolveFieldType(type); + if (fieldType == null) { + throw new MyRuntimeException("不支持的字段类型:" + type); + } + int pageNum = 1; + int pageSize = 100; + int synced = 0; + int skipped = 0; + int failed = 0; + while (true) { + PageMethod.startPage(pageNum, pageSize); + LambdaQueryWrapper wq = new LambdaQueryWrapper<>(); + if (StringUtils.isNotEmpty(tableType)) { + wq.eq(OnlFormHead::getTableType, tableType); + } + wq.orderByAsc(OnlFormHead::getId); + Page page = (Page) onlFormHeadService.list(wq); + if (page == null || page.isEmpty()) { + break; + } + for (OnlFormHead head : page) { + int result = syncFieldToHead(head, fieldName, fieldType, length, description); + if (result == 1) { + synced++; + } else if (result == 0) { + skipped++; + } else { + failed++; + } + } + if (page.size() < pageSize) { + break; + } + pageNum++; + } + return "字段同步完成:新增 " + synced + " 张表,跳过(已有该字段)" + skipped + " 张表,失败 " + failed + " 张表"; + } + + /** 单张表同步:已存在该字段返回 0,同步失败返回 -1,成功返回 1 */ + private int syncFieldToHead(OnlFormHead head, String fieldName, String fieldType, Integer length, String description) { + String columnName = fieldName.toLowerCase(); + long exists = onlFormFieldService.count(new LambdaQueryWrapper() + .eq(OnlFormField::getHeadId, head.getId()) + .eq(OnlFormField::getFieldName, columnName)); + if (exists > 0) { + return 0; + } + // 长度/小数位:varchar 默认 255;decimal 精度=length、小数位默认 2;datetime/timestamp 无长度 + Integer fieldLength = null; + Integer fieldPoint = null; + if (CustomUtil.FieldType_varchar.equals(fieldType)) { + fieldLength = length == null ? 255 : length; + } else if (CustomUtil.FieldType_decimal.equals(fieldType)) { + fieldLength = length; + fieldPoint = 2; + } else if (CustomUtil.FieldType_int.equals(fieldType) || CustomUtil.FieldType_bigint.equals(fieldType)) { + fieldLength = length; + } + // 插入 onl_form_field + OnlFormField field = new OnlFormField(); + field.setId(idGenerator.nextLongId()); + field.setHeadId(head.getId()); + field.setFieldName(columnName); + field.setFieldRemark(description); + field.setFieldType(fieldType); + field.setFieldLength(fieldLength); + field.setFieldPoint(fieldPoint); + field.setIsEmpty(1); + field.setCreateTime(new Date()); + onlFormFieldService.save(field); + // 未同步的表:只加字段,不同步 DB + if (!"1".equals(head.getStatus())) { + return 1; + } + // 已同步的表:syncDB 加列 + 回写 zz_online_column + try { + onlFormHeadService.syncDB(head, 0L); + } catch (Exception e) { + log.error("同步表结构失败,headId={},tableName={}", head.getId(), head.getTableName(), e); + return -1; + } + writeOnlineColumn(head, columnName, description); + return 1; + } + + /** 回写 zz_online_column:经 datasource → dblink 取新增列信息后 saveBySqlTable */ + private void writeOnlineColumn(OnlFormHead head, String columnName, String description) { + OnlineTable onlineTable = onlineTableService.getOne(new LambdaQueryWrapper() + .eq(OnlineTable::getTableName, head.getTableName())); + if (onlineTable == null) { + return; + } + OnlineDatasource datasource = onlineDatasourceService.getOne(new LambdaQueryWrapper() + .eq(OnlineDatasource::getMasterTableId, onlineTable.getTableId())); + if (datasource == null) { + return; + } + OnlineDblink dblink = onlineDblinkService.getById(datasource.getDblinkId()); + if (dblink == null) { + return; + } + List columns = onlineDblinkService.getDblinkTableColumnList(dblink, head.getTableName()); + if (CollUtil.isEmpty(columns)) { + return; + } + for (SqlTableColumn col : columns) { + if (columnName.equalsIgnoreCase(col.getColumnName())) { + onlineColumnService.saveBySqlTable(col, onlineTable.getTableId(), description); + return; + } + } + } + + /** 类型解析:数据库类型名 → onl_fieldType 字典码;直接传字典码则原样使用 */ + private String resolveFieldType(String type) { + if (StringUtils.isBlank(type)) { + return null; + } + String code = TYPE_CODE_MAP.get(type.toLowerCase()); + if (code != null) { + return code; + } + if (isFieldTypeCode(type)) { + return type; + } + return null; + } + + private boolean isFieldTypeCode(String type) { + return CustomUtil.FieldType_varchar.equals(type) || CustomUtil.FieldType_int.equals(type) + || CustomUtil.FieldType_bigint.equals(type) || CustomUtil.FieldType_datetime.equals(type) + || CustomUtil.FieldType_decimal.equals(type) || CustomUtil.FieldType_timestamp.equals(type); + } +} 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 df1e556..2a36b66 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 @@ -16,10 +16,13 @@ import apelet.common.online.dto.OnlineEventPluginExecuteDto; import apelet.common.online.model.*; import apelet.common.online.model.constant.EventEnum; import apelet.common.online.object.PluginInfo; +import apelet.common.online.rule.FormRuleEngine; +import apelet.common.online.rule.condition.ConditionEvaluator; +import apelet.common.online.rule.condition.FunctionExecutor; +import apelet.common.online.rule.model.RuleCondition; +import apelet.common.online.rule.model.RuleEvalContext; import apelet.common.online.service.*; -import apelet.common.online.util.Jexl3Util; import apelet.common.online.util.OnlineRedisKeyUtil; -import apelet.common.online.vo.SysFunctionVo; import apelet.common.orm.impl.EntityColumn; import apelet.common.orm.impl.EntityTable; import apelet.common.orm.impl.LinkEntityColumn; @@ -28,7 +31,6 @@ import apelet.common.sequence.wrapper.IdGeneratorWrapper; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.ObjectUtil; -import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; @@ -36,14 +38,13 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.github.pagehelper.Page; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.redisson.api.RBucket; import org.redisson.api.RedissonClient; -import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.ObjectUtils; import java.lang.reflect.Method; import java.util.*; @@ -90,9 +91,11 @@ public class OnlineFormServiceImpl extends BaseService impleme @Autowired private OrmGenDataSourceUtil ormGenDataSourceUtil; @Autowired - ISysFunctionService sysFunctionService; + FormRuleEngine formRuleEngine; @Autowired - Jexl3Util jexl3Util; + FunctionExecutor functionExecutor; + @Autowired + ConditionEvaluator conditionEvaluator; /** @@ -359,72 +362,14 @@ public class OnlineFormServiceImpl extends BaseService impleme } try { JSONObject jsonObject = JSONObject.parseObject(onlineForm.getWidgetJson()); - String deviceType = AppDeviceType.getDeviceType(); - JSONArray jsonArray = jsonObject.getJSONObject(deviceType).getJSONArray("pluginList"); - Model model = onlineEventPluginExecuteDto.getModel(); - Map masterData = model.getMasterData(); - Map savedata = model.getSlaveData(); - // if(masterData == null || savedata == null){ - // return onlineEventPluginExecuteVos; - // } - ObjectValue objectValue = null; - if (masterData != null) { - OnlineDatasource onlineDatasource = onlineDatasourceService.getOnlineDatasourceFromCache(model.getDatasourceId()); - objectValue = this.getObjectValue(masterData, savedata, onlineDatasource.getDatasourceName()); - objectValue.put("rowKey", onlineEventPluginExecuteDto.getRowKey()); - objectValue.put("rowKeySubset", onlineEventPluginExecuteDto.getRowKeySubset()); - objectValue.put("rowChange", model.getRowChange()); - // 处理条件规则 - // 界面规则 - JSONArray conditionTypeList = jsonObject.getJSONObject(AppDeviceType.getDeviceType()).getJSONArray("conditionTypeList"); - objectValue.put("conditional", this.analysisConditionalRules(conditionTypeList, objectValue)); - // 业务规则 - JSONArray businessRulesList = jsonObject.getJSONObject(AppDeviceType.getDeviceType()).getJSONArray("businessRulesList"); - objectValue.put("business", this.analysisConditionalRules(businessRulesList, objectValue)); - objectValue.put("keyNameObj", jsonObject.getJSONObject(AppDeviceType.getDeviceType()).getJSONObject("keyNameObj")); - }else{ - objectValue = new ObjectValue(null); - } - objectValue.put("onlineForm",onlineForm); - objectValue.put("listData",model.getListData()); - objectValue.put("rowData",model.getRowData()); - objectValue.put("eventParams",onlineEventPluginExecuteDto.getEventParams()); - objectValue.put("parentParams",onlineEventPluginExecuteDto.getParentParams()); - objectValue.put("innerval",onlineEventPluginExecuteDto.getInnerval()); - objectValue.put("returnData",model.getReturnData()); - List pluginInfos = new ArrayList<>(); - if (jsonArray != null) { - pluginInfos = jsonArray.toJavaList(PluginInfo.class); - } - PluginInfo pluginInfom = new PluginInfo(); - pluginInfom.setPluginMemo("界面规则"); - pluginInfom.setPluginType("apelet.common.online.abstractplugin.MyInterfaceRulesExecute"); - pluginInfom.setPluginName("MyInterfaceRulesExecute"); - pluginInfom.setOrder(-1); - pluginInfos.add(pluginInfom); - pluginInfos.sort(Comparator.comparing(PluginInfo::getOrder)); - log.info("eventPluginExecute, pluginInfos=>{}", JSON.toJSONString(pluginInfos)); - for (PluginInfo pluginInfo : pluginInfos) { - Class aClass = Class.forName(pluginInfo.getPluginType()); - if (onlineEventPluginExecuteDto.getEventCode() == null) { - continue; - } - Method method = aClass.getMethod(EventEnum.of(onlineEventPluginExecuteDto.getEventCode()).getCode(), String.class, ObjectValue.class); - Object object = aClass.newInstance(); - Method setDtoMethod = aClass.getMethod("setDto",OnlineEventPluginExecuteDto.class); - setDtoMethod.invoke(object, onlineEventPluginExecuteDto); - - method.invoke(object, onlineEventPluginExecuteDto.getEventWidgetVariableName(), objectValue); - Method resultMethod = aClass.getMethod("getResult"); - ObjectValue view = (ObjectValue) resultMethod.invoke(object);; - if (view != null) { - List result = (List) view.getValues().get("onlineEventPluginExecute"); - if (result != null && result.size() > 0) { - pluginResult.addAll(result); - } - } - //break; - } + JSONObject device = jsonObject.getJSONObject(AppDeviceType.getDeviceType()); + ObjectValue objectValue = buildObjectValue(onlineForm, device, onlineEventPluginExecuteDto); + // ② 规则引擎:解析 + 规则级求值 + 执行,直接产出前端指令(不再走插件反射) + pluginResult.addAll(formRuleEngine.execute(onlineForm, objectValue, onlineEventPluginExecuteDto.getEventCode(), onlineEventPluginExecuteDto)); + // ③ 自定义插件原样(只跑 widgetJson.pluginList,不再强制加 MyInterfaceRulesExecute) + List pluginInfos = device == null || device.getJSONArray("pluginList") == null + ? new ArrayList<>() : device.getJSONArray("pluginList").toJavaList(PluginInfo.class); + pluginResult.addAll(executePlugins(pluginInfos, onlineEventPluginExecuteDto, objectValue)); } catch (Exception e) { log.error(e.getMessage(), e); throw new RuntimeException(e); @@ -432,6 +377,90 @@ public class OnlineFormServiceImpl extends BaseService impleme return pluginResult; } + private ObjectValue buildObjectValue(OnlineForm onlineForm, JSONObject device, OnlineEventPluginExecuteDto onlineEventPluginExecuteDto) { + Model model = onlineEventPluginExecuteDto.getModel(); + Map masterData = model.getMasterData(); + Map savedata = model.getSlaveData(); + ObjectValue objectValue; + if (masterData != null) { + OnlineDatasource onlineDatasource = onlineDatasourceService.getOnlineDatasourceFromCache(model.getDatasourceId()); + objectValue = this.getObjectValue(masterData, savedata, onlineDatasource.getDatasourceName()); + objectValue.put("rowKey", onlineEventPluginExecuteDto.getRowKey()); + objectValue.put("rowKeySubset", onlineEventPluginExecuteDto.getRowKeySubset()); + objectValue.put("rowChange", model.getRowChange()); + objectValue.put("keyNameObj", device == null ? null : device.getJSONObject("keyNameObj")); + } else { + objectValue = new ObjectValue(null); + } + objectValue.put("onlineForm", onlineForm); + objectValue.put("listData", model.getListData()); + objectValue.put("rowData", model.getRowData()); + objectValue.put("eventParams", onlineEventPluginExecuteDto.getEventParams()); + objectValue.put("parentParams", onlineEventPluginExecuteDto.getParentParams()); + objectValue.put("innerval", onlineEventPluginExecuteDto.getInnerval()); + objectValue.put("returnData", model.getReturnData()); + return objectValue; + } + + /** 自定义插件反射执行循环,原样保留(不再强制追加 MyInterfaceRulesExecute) */ + private List executePlugins(List pluginInfos, OnlineEventPluginExecuteDto onlineEventPluginExecuteDto, ObjectValue objectValue) throws Exception { + List pluginResult = new ArrayList<>(); + for (PluginInfo pluginInfo : pluginInfos) { + if (onlineEventPluginExecuteDto.getEventCode() == null) { + continue; + } + Class aClass = Class.forName(pluginInfo.getPluginType()); + Method method = aClass.getMethod(EventEnum.of(onlineEventPluginExecuteDto.getEventCode()).getCode(), String.class, ObjectValue.class); + Object object = aClass.newInstance(); + Method setDtoMethod = aClass.getMethod("setDto", OnlineEventPluginExecuteDto.class); + setDtoMethod.invoke(object, onlineEventPluginExecuteDto); + method.invoke(object, onlineEventPluginExecuteDto.getEventWidgetVariableName(), objectValue); + Method resultMethod = aClass.getMethod("getResult"); + ObjectValue view = (ObjectValue) resultMethod.invoke(object); + if (view != null) { + List result = (List) view.getValues().get("onlineEventPluginExecute"); + if (result != null && !result.isEmpty()) { + pluginResult.addAll(result); + } + } + } + return pluginResult; + } + + /** + * 保留:内置插件(Save/Audit/Submit OperationServicePlugIn)仍调用,委托新 ConditionEvaluator(规则级求值)。 + */ + public Object func(String s, JSONObject parsed, ObjectValue objectValue) { + RuleCondition condition = new RuleCondition(); + condition.setExpression(s); + condition.setFunctionForm(parsed == null ? null : parseFunctionForm(parsed.getJSONArray("functionForm"))); + return conditionEvaluator.evaluate(condition, new RuleEvalContext(objectValue, null)); + } + + /** + * 保留:SaveOperationServicePlugIn / AuditOperationServicePlugIn 等内置插件仍调用,委托 FunctionExecutor。 + */ + public Object executeFunction(JSONObject parsed, String fun, Object[] objects) { + return functionExecutor.executeWithParams(parseFunctionForm(parsed.getJSONArray("functionForm")), fun, objects); + } + + private Map parseFunctionForm(JSONArray functionForm) { + Map map = new HashMap<>(); + if (functionForm == null) { + return map; + } + for (Object o : functionForm) { + JSONObject json = (JSONObject) o; + for (String key : json.keySet()) { + String value = json.getString(key); + if (StringUtils.isNumeric(value)) { + map.put(key, Long.parseLong(value)); + } + } + } + return map; + } + @Override public ObjectValue getObjectValue(Map masterData, Map savedata, String tableName) { EntityTable entityTable = ormGenDataSourceUtil.getEntityTable(tableName); @@ -524,81 +553,4 @@ public class OnlineFormServiceImpl extends BaseService impleme } } - /** - * 处理 条件/业务 规则 - */ - public List> analysisConditionalRules(JSONArray jsonArray, ObjectValue objectValue) { - List> list = new ArrayList<>(); - // 获取页面处理规则 - if (CollUtil.isEmpty(jsonArray)) { - return list; - } - for (Object o : jsonArray) { - Map hashMap = new HashMap<>(); - JSONObject parsed = JSON.parseObject(o.toString()); - if (parsed.getString("Enable").equals("false")) { - continue; - } - String s = parsed.getString("expressionTranslation"); - // 规则表达式 - Object result = func(s, parsed, objectValue); - hashMap.put("establish", result); - // 表达式包含的字段属性名称 - if (ObjectUtils.isEmpty(parsed.get("nameList"))) { - break; - } - hashMap.put("functionForm", parsed.getJSONArray("functionForm")); - hashMap.put("expression", s); -// JSONObject jsonObject1 = JSON.parseObject(parsed.get("nameList").toString()); -// Map mapValue = getMapValue(jsonObject1, objectValue); -// hashMap.put("establish", established(s, mapValue)); - hashMap.put("allList", parsed.getJSONArray("allList")); - list.add(hashMap); - } - return list; - } - - - public Object func(String s, JSONObject parsed, ObjectValue objectValue) { - List> alist = new ArrayList<>(); - jexl3Util.splitExpression(s, alist); - boolean result = true; - for (Map map : alist) { - String expression = map.get("expression").toString(); - if (expression.startsWith("$")) { - String fun = jexl3Util.getMySplit("\\$(.*?)\\$", expression); - List funParam = Arrays.asList(jexl3Util.getMySplit("\\((.*?)\\)", fun).split(",")); - Object[] objects = jexl3Util.getFunParam(funParam, objectValue); - //执行函数 - Object o = executeFunction(parsed, fun, objects); - result = jexl3Util.judgmentFunc(expression, o) && result; - } else { - // 表达式 - boolean b = jexl3Util.getExpressionValue(expression, objectValue); - result = b && result; - } - } - return result; - } - - // 执行函数 - public Object executeFunction(JSONObject parsed, String fun, Object[] objects) { - JSONArray array = parsed.getJSONArray("functionForm"); - String finalFun = fun.substring(0, fun.indexOf('(')).trim(); - Long funId = 0L; - for (Object object : array) { - JSONObject json = (JSONObject) object; - if (!json.containsKey(finalFun)) { - continue; - } - funId = (json.getLong(finalFun)); - } - SysFunction sysFunction = sysFunctionService.getById(funId); - SysFunctionVo sysFunctionVo = new SysFunctionVo(); - BeanUtils.copyProperties(sysFunction, sysFunctionVo); - sysFunctionVo.setParameter(objects); - return sysFunctionService.onLineFunction(sysFunctionVo); - } - - } diff --git a/common/common-online/src/main/java/apelet/common/online/util/Jexl3Util.java b/common/common-online/src/main/java/apelet/common/online/util/Jexl3Util.java index 76e0137..a8306eb 100644 --- a/common/common-online/src/main/java/apelet/common/online/util/Jexl3Util.java +++ b/common/common-online/src/main/java/apelet/common/online/util/Jexl3Util.java @@ -1,9 +1,7 @@ package apelet.common.online.util; -import apelet.common.core.constant.AppDeviceType; import apelet.common.core.object.ObjectCollection; import apelet.common.core.object.ObjectValue; -import apelet.common.core.object.TokenData; import apelet.common.generator.utils.OrmGenDataSourceUtil; import apelet.common.orm.impl.Filter; import apelet.common.orm.impl.FilterItem; @@ -42,9 +40,7 @@ public class Jexl3Util { * @return */ public Object expression(String expressionStr, Map paramMap) { - expressionStr = expressionStr.toLowerCase() - .replace("not", " != ") - .replace("null", "''"); + expressionStr = expressionStr.toLowerCase().replaceAll("\\bnot\\b", " != ").replaceAll("\\bnull\\b", "''"); // 创建表达式引擎对象 JexlEngine engine = new JexlBuilder().create(); // 创建Context对象,为表达式中的未知数赋值 @@ -73,6 +69,7 @@ public class Jexl3Util { map.put(tab + "." + tab + "." + object, v); } else { ObjectCollection o1 = (ObjectCollection) objectValue.get(tab); + if (o1 == null) continue; // 附表为空,跳过 Iterator iterator = o1.iterator(); while (iterator.hasNext()) { ObjectValue next = (ObjectValue) iterator.next(); @@ -112,6 +109,7 @@ public class Jexl3Util { } else { List objects = new ArrayList<>(); ObjectCollection o = (ObjectCollection) objectValue.get(strings[1]); + if (o == null) return Collections.emptyList(); // 分录表为空,返回空参数 Iterator iterator = o.iterator(); while (iterator.hasNext()) { ObjectValue next = (ObjectValue) iterator.next(); @@ -231,6 +229,79 @@ public class Jexl3Util { return ormGenDataSourceUtil.query(table, filter, null).getObject(0); } + /** + * 当前编辑行:rowKeySubset 命中的行;未过滤时返回无 rowkey 的新增行,都没有则取第一行。 + */ + public ObjectValue currentRow(ObjectCollection collection, String rowKeySubset) { + if (collection == null || collection.isEmpty()) return null; + Set targetSet = new HashSet<>(); + if (StringUtils.isNotEmpty(rowKeySubset)) Collections.addAll(targetSet, rowKeySubset.split(",")); + Iterator iterator = collection.iterator(); + while (iterator.hasNext()) { + ObjectValue next = (ObjectValue) iterator.next(); + if ((StringUtils.isEmpty(rowKeySubset) && next.get("rowkey") != null) + || (next.get("rowkey") != null && !targetSet.contains(next.get("rowkey")))) continue; + return next; + } + return null; + } + + /** + * 当前编辑分录行:rowKeySubset 命中(行内编辑多行)优先;其次主对象 rowKey 命中(分录编辑完成事件,rowKey=当前编辑行);都无则返回无 rowkey 的新增行。 + */ + public ObjectValue currentRow(ObjectCollection collection, String rowKeySubset, String rowKey) { + if (collection == null || collection.isEmpty()) return null; + if (StringUtils.isNotEmpty(rowKeySubset)) { + ObjectValue row = currentRow(collection, rowKeySubset); + if (row != null) return row; + } + if (StringUtils.isNotEmpty(rowKey)) { + Iterator iterator = collection.iterator(); + while (iterator.hasNext()) { + ObjectValue next = (ObjectValue) iterator.next(); + if (rowKey.equals(next.getString("rowkey"))) return next; + } + } + return currentRow(collection, null); + } + + /** + * 灵活解析字段引用到实际值。 + * 支持 表.表.字段 / 表.字段 / 表.关联对象.子字段(主表)、主表.分录表.字段 / 主表.分录表.关联对象.子字段(分录取当前编辑行); + * 关联对象(ObjectValue)自动下钻任意深度。 + */ + public Object resolveRefValue(String ref, ObjectValue objectValue, String rowKeySubset) { + String[] parts = ref.split("\\."); + if (parts.length < 2) return ""; + boolean masterPrefix = parts[0].equals(parts[1]); + int start = masterPrefix ? 2 : 1; + if (start >= parts.length) return ""; + Object v = objectValue.get(parts[start]); + if (!masterPrefix && v instanceof ObjectCollection) { + ObjectValue row = currentRow((ObjectCollection) v, rowKeySubset, (String) objectValue.get("rowKey")); + return row == null ? "" : resolveFieldChain(row, parts, start + 1); + } + return resolveFieldChain(objectValue, parts, start); + } + + /** + * 从给定对象起,按字段链逐段解析;遇到 ObjectValue(关联表弹窗)自动下钻,任意深度,最后一段返回实际值 + */ + public Object resolveFieldChain(ObjectValue current, String[] parts, int from) { + ObjectValue cur = current; + for (int i = from; i < parts.length; i++) { + if (cur == null) return ""; + Object v = cur.get(parts[i]); + if (i == parts.length - 1) return v == null ? "" : v; + if (v instanceof ObjectValue) { + cur = getObjectValue(((ObjectValue) v).get("id"), ((ObjectValue) v).getTableName()); + } else { + return v == null ? "" : v; + } + } + return ""; + } + public boolean judgmentFunc(String expression, Object value) { String trim = expression.substring(expression.indexOf(")") + 1).trim(); trim = "functionValue" + " " + trim.substring(0, trim.length() - 1); 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 a9818e9..199d6ee 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 @@ -9,8 +9,10 @@ import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import jodd.util.StringUtil; +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.function.Consumer; @@ -199,6 +201,55 @@ public class WidgetJsonUtil { } /** + * 收集 pc/mobile 控件树中 列名(columnFieldName) → 控件 variableName 列表。 + * 仅收集 pc/mobile 的 widgetList(叶子字段控件),递归 childWidgetList 下钻,与 buildTableFieldMap 取值一致; + * 一个列名可对应多个控件(多个控件绑定同一列)时返回多个 variableName。 + * + * @param widgetJson 在线表单的 widgetJson + * @return 列名 → variableName 列表 + */ + public static Map> collectColumnVariableNames(JSONObject widgetJson) { + Map> map = new HashMap<>(); + if (widgetJson == null) return map; + for (String mode : new String[]{"pc", "mobile"}) { + JSONObject modeObj = widgetJson.getJSONObject(mode); + if (modeObj == null) continue; + collectColumnVariableNames(modeObj.getJSONArray("widgetList"), map); + } + return map; + } + + private static void collectColumnVariableNames(JSONArray widgetList, Map> map) { + if (CollUtil.isEmpty(widgetList)) { + return; + } + for (int i = 0; i < widgetList.size(); i++) { + JSONObject widget = widgetList.getJSONObject(i); + if (widget == null) { + continue; + } + JSONArray childWidgetList = widget.getJSONArray("childWidgetList"); + if (CollUtil.isNotEmpty(childWidgetList)) { + collectColumnVariableNames(childWidgetList, map); + continue; + } + JSONObject bindData = widget.getJSONObject("bindData"); + if (bindData == null) { + continue; + } + String columnFieldName = bindData.getString("columnFieldName"); + String variableName = widget.getString("variableName"); + if (StrUtil.isEmpty(columnFieldName) || StrUtil.isEmpty(variableName)) { + continue; + } + List names = map.computeIfAbsent(columnFieldName, k -> new ArrayList<>()); + if (!names.contains(variableName)) { + names.add(variableName); + } + } + } + + /** * 递归收集控件树中的"表-字段"绑定关系。 */ private static void collectTableFields(JSONArray widgetList, Map> tableFieldMap) {