打印了JENKINS节点日志
This commit is contained in:
parent
211c00f930
commit
2f2637763c
@ -1,541 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.framework.utils;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.flowable.engine.delegate.DelegateExecution;
|
|
||||||
import org.flowable.task.service.delegate.DelegateTask;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 嵌套 Map 工具类
|
|
||||||
* 使用 Jackson JsonPointer (RFC 6901) 标准解析嵌套路径
|
|
||||||
*
|
|
||||||
* @author qqchen
|
|
||||||
* @since 2025-11-04
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class NestedMapUtils {
|
|
||||||
|
|
||||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从嵌套 Map 中获取值
|
|
||||||
* 支持多层嵌套路径访问,例如:outputs.status、a.b.c.d
|
|
||||||
*
|
|
||||||
* <p>示例:
|
|
||||||
* <pre>
|
|
||||||
* Map<String, Object> map = Map.of("outputs", Map.of("status", "SUCCESS"));
|
|
||||||
* Object value = NestedMapUtils.getValue(map, "outputs.status");
|
|
||||||
* // 返回: "SUCCESS"
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* @param map 源 Map
|
|
||||||
* @param fieldPath 字段路径(点号分隔,如:outputs.status)
|
|
||||||
* @return 字段值,如果路径不存在则返回 null
|
|
||||||
*/
|
|
||||||
public static Object getValue(Map<String, Object> map, String fieldPath) {
|
|
||||||
if (fieldPath == null || fieldPath.isEmpty() || map == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. 将 Map 转换为 JsonNode
|
|
||||||
JsonNode rootNode = OBJECT_MAPPER.valueToTree(map);
|
|
||||||
|
|
||||||
// 2. 将点号路径转换为 JsonPointer 路径格式
|
|
||||||
// 例如:outputs.status -> /outputs/status
|
|
||||||
String jsonPointerPath = "/" + fieldPath.replace(".", "/");
|
|
||||||
|
|
||||||
// 3. 使用 JsonPointer 访问嵌套路径
|
|
||||||
JsonNode resultNode = rootNode.at(jsonPointerPath);
|
|
||||||
|
|
||||||
// 4. 检查节点是否存在
|
|
||||||
if (resultNode.isMissingNode()) {
|
|
||||||
log.debug("嵌套路径不存在: {}", fieldPath);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 将 JsonNode 转换回 Java 对象
|
|
||||||
return OBJECT_MAPPER.convertValue(resultNode, Object.class);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("嵌套路径解析异常: {} - {}", fieldPath, e.getMessage());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从嵌套 Map 中获取字符串值
|
|
||||||
*
|
|
||||||
* @param map 源 Map
|
|
||||||
* @param fieldPath 字段路径(点号分隔)
|
|
||||||
* @return 字符串值,如果路径不存在或值为 null 则返回 null
|
|
||||||
*/
|
|
||||||
public static String getString(Map<String, Object> map, String fieldPath) {
|
|
||||||
Object value = getValue(map, fieldPath);
|
|
||||||
return value != null ? value.toString() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从嵌套 Map 中获取字符串值(带默认值)
|
|
||||||
*
|
|
||||||
* @param map 源 Map
|
|
||||||
* @param fieldPath 字段路径(点号分隔)
|
|
||||||
* @param defaultValue 默认值
|
|
||||||
* @return 字符串值,如果路径不存在则返回默认值
|
|
||||||
*/
|
|
||||||
public static String getString(Map<String, Object> map, String fieldPath, String defaultValue) {
|
|
||||||
String value = getString(map, fieldPath);
|
|
||||||
return value != null ? value : defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从嵌套 Map 中获取 Integer 值
|
|
||||||
*
|
|
||||||
* @param map 源 Map
|
|
||||||
* @param fieldPath 字段路径(点号分隔)
|
|
||||||
* @return Integer 值,如果路径不存在或转换失败则返回 null
|
|
||||||
*/
|
|
||||||
public static Integer getInteger(Map<String, Object> map, String fieldPath) {
|
|
||||||
Object value = getValue(map, fieldPath);
|
|
||||||
if (value == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (value instanceof Integer) {
|
|
||||||
return (Integer) value;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return Integer.valueOf(value.toString());
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
log.warn("无法将值转换为 Integer: {} = {}", fieldPath, value);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从嵌套 Map 中获取 Long 值
|
|
||||||
*
|
|
||||||
* @param map 源 Map
|
|
||||||
* @param fieldPath 字段路径(点号分隔)
|
|
||||||
* @return Long 值,如果路径不存在或转换失败则返回 null
|
|
||||||
*/
|
|
||||||
public static Long getLong(Map<String, Object> map, String fieldPath) {
|
|
||||||
Object value = getValue(map, fieldPath);
|
|
||||||
if (value == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (value instanceof Long) {
|
|
||||||
return (Long) value;
|
|
||||||
}
|
|
||||||
if (value instanceof Integer) {
|
|
||||||
return ((Integer) value).longValue();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return Long.valueOf(value.toString());
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
log.warn("无法将值转换为 Long: {} = {}", fieldPath, value);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从嵌套 Map 中获取 Boolean 值
|
|
||||||
*
|
|
||||||
* @param map 源 Map
|
|
||||||
* @param fieldPath 字段路径(点号分隔)
|
|
||||||
* @return Boolean 值,如果路径不存在则返回 null
|
|
||||||
*/
|
|
||||||
public static Boolean getBoolean(Map<String, Object> map, String fieldPath) {
|
|
||||||
Object value = getValue(map, fieldPath);
|
|
||||||
if (value == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (value instanceof Boolean) {
|
|
||||||
return (Boolean) value;
|
|
||||||
}
|
|
||||||
return Boolean.valueOf(value.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从嵌套 Map 中获取指定类型的值
|
|
||||||
*
|
|
||||||
* @param map 源 Map
|
|
||||||
* @param fieldPath 字段路径(点号分隔)
|
|
||||||
* @param targetClass 目标类型
|
|
||||||
* @param <T> 返回值类型
|
|
||||||
* @return 指定类型的值,如果路径不存在或转换失败则返回 null
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public static <T> T getValue(Map<String, Object> map, String fieldPath, Class<T> targetClass) {
|
|
||||||
Object value = getValue(map, fieldPath);
|
|
||||||
if (value == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (targetClass.isInstance(value)) {
|
|
||||||
return (T) value;
|
|
||||||
}
|
|
||||||
return OBJECT_MAPPER.convertValue(value, targetClass);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("无法将值转换为 {}: {} = {}", targetClass.getSimpleName(), fieldPath, value);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查路径是否存在
|
|
||||||
*
|
|
||||||
* @param map 源 Map
|
|
||||||
* @param fieldPath 字段路径(点号分隔)
|
|
||||||
* @return true 如果路径存在,否则返回 false
|
|
||||||
*/
|
|
||||||
public static boolean hasPath(Map<String, Object> map, String fieldPath) {
|
|
||||||
if (fieldPath == null || fieldPath.isEmpty() || map == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
JsonNode rootNode = OBJECT_MAPPER.valueToTree(map);
|
|
||||||
String jsonPointerPath = "/" + fieldPath.replace(".", "/");
|
|
||||||
JsonNode resultNode = rootNode.at(jsonPointerPath);
|
|
||||||
return !resultNode.isMissingNode();
|
|
||||||
} catch (Exception e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== Flowable 集成方法 ====================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从 Flowable 执行上下文中解析变量表达式
|
|
||||||
* 支持嵌套路径,例如:approval.userIds、sid_xxx.outputs.status
|
|
||||||
*
|
|
||||||
* <p>示例:
|
|
||||||
* <pre>
|
|
||||||
* // 流程变量中有:approval = {required: true, userIds: "admin"}
|
|
||||||
* Object value = NestedMapUtils.getValueFromExecution(execution, "approval.userIds");
|
|
||||||
* // 返回: "admin"
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* @param execution Flowable 执行上下文
|
|
||||||
* @param variablePath 变量路径(点号分隔,如:approval.userIds)
|
|
||||||
* @return 变量值,如果路径不存在则返回 null
|
|
||||||
*/
|
|
||||||
public static Object getValueFromExecution(DelegateExecution execution, String variablePath) {
|
|
||||||
if (execution == null || variablePath == null || variablePath.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 切分路径,获取顶层变量名
|
|
||||||
String[] parts = variablePath.split("\\.", 2);
|
|
||||||
String topLevelVar = parts[0];
|
|
||||||
|
|
||||||
// 2. 从 execution 中获取顶层变量
|
|
||||||
Object value = execution.getVariable(topLevelVar);
|
|
||||||
|
|
||||||
// 3. 如果没有嵌套路径,直接返回
|
|
||||||
if (parts.length == 1) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 如果有嵌套路径,继续解析
|
|
||||||
if (value instanceof Map) {
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Map<String, Object> map = (Map<String, Object>) value;
|
|
||||||
return getValue(map, parts[1]);
|
|
||||||
} else {
|
|
||||||
log.debug("变量 {} 不是 Map 类型,无法解析嵌套路径: {}", topLevelVar, variablePath);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从 Flowable 任务上下文中解析变量表达式
|
|
||||||
* 支持嵌套路径,例如:approval.userIds
|
|
||||||
*
|
|
||||||
* <p>示例:
|
|
||||||
* <pre>
|
|
||||||
* // 流程变量中有:approval = {required: true, userIds: "admin"}
|
|
||||||
* Object value = NestedMapUtils.getValueFromTask(task, "approval.userIds");
|
|
||||||
* // 返回: "admin"
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* @param task Flowable 任务上下文
|
|
||||||
* @param variablePath 变量路径(点号分隔,如:approval.userIds)
|
|
||||||
* @return 变量值,如果路径不存在则返回 null
|
|
||||||
*/
|
|
||||||
public static Object getValueFromTask(DelegateTask task, String variablePath) {
|
|
||||||
if (task == null || variablePath == null || variablePath.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 切分路径,获取顶层变量名
|
|
||||||
String[] parts = variablePath.split("\\.", 2);
|
|
||||||
String topLevelVar = parts[0];
|
|
||||||
|
|
||||||
// 2. 从 task 中获取顶层变量
|
|
||||||
Object value = task.getVariable(topLevelVar);
|
|
||||||
|
|
||||||
// 3. 如果没有嵌套路径,直接返回
|
|
||||||
if (parts.length == 1) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 如果有嵌套路径,继续解析
|
|
||||||
if (value instanceof Map) {
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Map<String, Object> map = (Map<String, Object>) value;
|
|
||||||
return getValue(map, parts[1]);
|
|
||||||
} else {
|
|
||||||
log.debug("变量 {} 不是 Map 类型,无法解析嵌套路径: {}", topLevelVar, variablePath);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从 Flowable 上下文中解析变量表达式(支持 ${...} 格式)
|
|
||||||
* 自动去除表达式包装,支持嵌套路径
|
|
||||||
*
|
|
||||||
* <p>示例:
|
|
||||||
* <pre>
|
|
||||||
* Object value1 = NestedMapUtils.resolveExpression(task, "${approval.userIds}");
|
|
||||||
* Object value2 = NestedMapUtils.resolveExpression(task, "approval.userIds");
|
|
||||||
* // 两种格式都能正确解析
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* @param task Flowable 任务上下文
|
|
||||||
* @param expression 表达式(可以带 ${} 也可以不带)
|
|
||||||
* @return 变量值,如果路径不存在则返回 null
|
|
||||||
*/
|
|
||||||
public static Object resolveExpression(DelegateTask task, String expression) {
|
|
||||||
if (expression == null || expression.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 去除 ${} 包装
|
|
||||||
String variablePath = expression;
|
|
||||||
if (expression.startsWith("${") && expression.endsWith("}")) {
|
|
||||||
variablePath = expression.substring(2, expression.length() - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return getValueFromTask(task, variablePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从 Flowable 执行上下文中解析变量表达式(支持 ${...} 格式)
|
|
||||||
*
|
|
||||||
* @param execution Flowable 执行上下文
|
|
||||||
* @param expression 表达式(可以带 ${} 也可以不带)
|
|
||||||
* @return 变量值,如果路径不存在则返回 null
|
|
||||||
*/
|
|
||||||
public static Object resolveExpression(DelegateExecution execution, String expression) {
|
|
||||||
if (expression == null || expression.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 去除 ${} 包装
|
|
||||||
String variablePath = expression;
|
|
||||||
if (expression.startsWith("${") && expression.endsWith("}")) {
|
|
||||||
variablePath = expression.substring(2, expression.length() - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return getValueFromExecution(execution, variablePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 统一解析入口(推荐使用) ====================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 统一解析方法 - 智能处理所有类型的输入
|
|
||||||
*
|
|
||||||
* <p>自动识别并处理三种情况:
|
|
||||||
* <ul>
|
|
||||||
* <li>纯文本: "hello" → "hello"</li>
|
|
||||||
* <li>纯表达式: "${user.name}" → Object (原始类型,可能是 String/Integer/Map 等)</li>
|
|
||||||
* <li>模板字符串: "Hello ${user.name}, age: ${user.age}" → "Hello John, age: 30"</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* <p>使用示例:
|
|
||||||
* <pre>
|
|
||||||
* // 纯表达式 - 返回原始类型
|
|
||||||
* Object status = NestedMapUtils.resolve(execution, "${jenkins.status}");
|
|
||||||
* // 返回: NodeExecutionStatusEnum.SUCCESS (枚举类型)
|
|
||||||
*
|
|
||||||
* // 模板字符串 - 返回替换后的字符串
|
|
||||||
* String message = (String) NestedMapUtils.resolve(execution, "构建${jenkins.status}, 编号: ${jenkins.buildNumber}");
|
|
||||||
* // 返回: "构建SUCCESS, 编号: 123"
|
|
||||||
*
|
|
||||||
* // 纯文本 - 直接返回
|
|
||||||
* String text = (String) NestedMapUtils.resolve(execution, "纯文本");
|
|
||||||
* // 返回: "纯文本"
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* @param execution Flowable 执行上下文
|
|
||||||
* @param input 输入字符串(可能包含表达式)
|
|
||||||
* @return 解析后的值(可能是任意类型)
|
|
||||||
*/
|
|
||||||
public static Object resolve(DelegateExecution execution, String input) {
|
|
||||||
if (input == null || !input.contains("${")) {
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 判断是否是纯表达式(整个字符串就是一个 ${xxx})
|
|
||||||
if (isPureExpression(input)) {
|
|
||||||
// 纯表达式:返回原始类型(可能是 String/Integer/Enum/Map 等)
|
|
||||||
return resolveExpression(execution, input);
|
|
||||||
} else {
|
|
||||||
// 模板字符串:返回替换后的字符串
|
|
||||||
return resolveTemplateString(execution, input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 统一解析方法 - DelegateTask 版本
|
|
||||||
*
|
|
||||||
* @param task Flowable 任务上下文
|
|
||||||
* @param input 输入字符串
|
|
||||||
* @return 解析后的值
|
|
||||||
*/
|
|
||||||
public static Object resolve(DelegateTask task, String input) {
|
|
||||||
if (input == null || !input.contains("${")) {
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPureExpression(input)) {
|
|
||||||
return resolveExpression(task, input);
|
|
||||||
} else {
|
|
||||||
return resolveTemplateString(task, input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断是否是纯表达式
|
|
||||||
* 纯表达式定义:整个字符串就是一个 ${xxx},前后没有其他内容
|
|
||||||
*
|
|
||||||
* @param input 输入字符串
|
|
||||||
* @return true 如果是纯表达式
|
|
||||||
*/
|
|
||||||
private static boolean isPureExpression(String input) {
|
|
||||||
if (!input.startsWith("${") || !input.endsWith("}")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否只有一个表达式(没有第二个 ${)
|
|
||||||
int secondExprStart = input.indexOf("${", 2);
|
|
||||||
return secondExprStart == -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 内部实现方法 ====================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析模板字符串(支持多个嵌入式表达式)
|
|
||||||
*
|
|
||||||
* <p>内部方法,推荐使用 {@link #resolve(DelegateExecution, String)} 代替
|
|
||||||
*
|
|
||||||
* @param execution Flowable 执行上下文
|
|
||||||
* @param template 模板字符串
|
|
||||||
* @return 替换后的字符串
|
|
||||||
*/
|
|
||||||
private static String resolveTemplateString(DelegateExecution execution, String template) {
|
|
||||||
if (template == null || !template.contains("${")) {
|
|
||||||
return template;
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder result = new StringBuilder();
|
|
||||||
int currentIndex = 0;
|
|
||||||
|
|
||||||
while (currentIndex < template.length()) {
|
|
||||||
int startIndex = template.indexOf("${", currentIndex);
|
|
||||||
|
|
||||||
// 没有更多的表达式了
|
|
||||||
if (startIndex == -1) {
|
|
||||||
result.append(template.substring(currentIndex));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加表达式前的普通文本
|
|
||||||
if (startIndex > currentIndex) {
|
|
||||||
result.append(template.substring(currentIndex, startIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查找表达式的结束位置
|
|
||||||
int endIndex = template.indexOf("}", startIndex);
|
|
||||||
if (endIndex == -1) {
|
|
||||||
// 没有找到闭合的 },说明格式错误,保留原样
|
|
||||||
log.warn("未找到闭合的 }} 符号,保留原样: {}", template.substring(startIndex));
|
|
||||||
result.append(template.substring(startIndex));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提取并解析表达式
|
|
||||||
String expression = template.substring(startIndex, endIndex + 1);
|
|
||||||
try {
|
|
||||||
Object value = resolveExpression(execution, expression);
|
|
||||||
result.append(value != null ? value.toString() : "");
|
|
||||||
log.debug(" 替换表达式: {} -> {}", expression, value);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn(" 表达式解析失败: {},保留原样", expression, e);
|
|
||||||
result.append(expression);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentIndex = endIndex + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析模板字符串(支持多个嵌入式表达式)- DelegateTask 版本
|
|
||||||
*
|
|
||||||
* <p>内部方法,推荐使用 {@link #resolve(DelegateTask, String)} 代替
|
|
||||||
*
|
|
||||||
* @param task Flowable 任务上下文
|
|
||||||
* @param template 模板字符串
|
|
||||||
* @return 替换后的字符串
|
|
||||||
*/
|
|
||||||
private static String resolveTemplateString(DelegateTask task, String template) {
|
|
||||||
if (template == null || !template.contains("${")) {
|
|
||||||
return template;
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder result = new StringBuilder();
|
|
||||||
int currentIndex = 0;
|
|
||||||
|
|
||||||
while (currentIndex < template.length()) {
|
|
||||||
int startIndex = template.indexOf("${", currentIndex);
|
|
||||||
|
|
||||||
if (startIndex == -1) {
|
|
||||||
result.append(template.substring(currentIndex));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (startIndex > currentIndex) {
|
|
||||||
result.append(template.substring(currentIndex, startIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
int endIndex = template.indexOf("}", startIndex);
|
|
||||||
if (endIndex == -1) {
|
|
||||||
log.warn("未找到闭合的 }} 符号,保留原样: {}", template.substring(startIndex));
|
|
||||||
result.append(template.substring(startIndex));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
String expression = template.substring(startIndex, endIndex + 1);
|
|
||||||
try {
|
|
||||||
Object value = resolveExpression(task, expression);
|
|
||||||
result.append(value != null ? value.toString() : "");
|
|
||||||
log.debug(" 替换表达式: {} -> {}", expression, value);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn(" 表达式解析失败: {},保留原样", expression, e);
|
|
||||||
result.append(expression);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentIndex = endIndex + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -0,0 +1,484 @@
|
|||||||
|
package com.qqchen.deploy.backend.framework.utils;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.flowable.engine.delegate.DelegateExecution;
|
||||||
|
import org.flowable.task.service.delegate.DelegateTask;
|
||||||
|
import org.springframework.expression.Expression;
|
||||||
|
import org.springframework.expression.ExpressionParser;
|
||||||
|
import org.springframework.expression.PropertyAccessor;
|
||||||
|
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||||
|
import org.springframework.expression.spel.support.SimpleEvaluationContext;
|
||||||
|
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||||
|
import org.springframework.expression.EvaluationContext;
|
||||||
|
import org.springframework.expression.AccessException;
|
||||||
|
import org.springframework.expression.TypedValue;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring EL 表达式解析工具
|
||||||
|
*
|
||||||
|
* <p>支持三种表达式格式:
|
||||||
|
* <ul>
|
||||||
|
* <li>变量引用: <code>${deploy.applicationName}</code> → "SSO管理后台"</li>
|
||||||
|
* <li>条件判断: <code>${deploy.status == 'SUCCESS' ? '成功' : '失败'}</code> → "成功"</li>
|
||||||
|
* <li>复杂运算: <code>${deploy.duration > 60 ? '耗时较长' : '正常'}</code> → "耗时较长"</li>
|
||||||
|
* <li>模板字符串: <code>"应用 ${deploy.applicationName} 部署${deploy.status == 'SUCCESS' ? '成功' : '失败'}"</code></li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>使用示例:
|
||||||
|
* <pre>{@code
|
||||||
|
* Map<String, Object> variables = new HashMap<>();
|
||||||
|
* Map<String, Object> deploy = new HashMap<>();
|
||||||
|
* deploy.put("applicationName", "SSO管理后台");
|
||||||
|
* deploy.put("status", "SUCCESS");
|
||||||
|
* deploy.put("duration", 120);
|
||||||
|
* variables.put("deploy", deploy);
|
||||||
|
*
|
||||||
|
* // 简单变量
|
||||||
|
* String result1 = SpelExpressionResolver.resolve("${deploy.applicationName}需要部署", variables);
|
||||||
|
* // 结果: "SSO管理后台需要部署"
|
||||||
|
*
|
||||||
|
* // 条件判断
|
||||||
|
* String result2 = SpelExpressionResolver.resolve(
|
||||||
|
* "部署${deploy.status == 'SUCCESS' ? '成功' : '失败'}", variables);
|
||||||
|
* // 结果: "部署成功"
|
||||||
|
*
|
||||||
|
* // 复杂表达式
|
||||||
|
* String result3 = SpelExpressionResolver.resolve(
|
||||||
|
* "应用 ${deploy.applicationName} 部署耗时 ${deploy.duration > 60 ? '较长' : '正常'}", variables);
|
||||||
|
* // 结果: "应用 SSO管理后台 部署耗时 较长"
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-11-04
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class SpelExpressionResolver {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表达式解析器(线程安全,可复用)
|
||||||
|
*/
|
||||||
|
private static final ExpressionParser PARSER = new SpelExpressionParser();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表达式匹配正则:${...}
|
||||||
|
* 使用非贪婪匹配,支持嵌套表达式
|
||||||
|
*/
|
||||||
|
private static final Pattern EXPRESSION_PATTERN = Pattern.compile("\\$\\{([^}]+)\\}");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析包含 EL 表达式的字符串模板
|
||||||
|
*
|
||||||
|
* @param template 模板字符串,可包含多个 ${...} 表达式
|
||||||
|
* @param variables 上下文变量(支持嵌套 Map)
|
||||||
|
* @return 解析后的字符串
|
||||||
|
*/
|
||||||
|
public static String resolve(String template, Map<String, Object> variables) {
|
||||||
|
if (template == null || template.isEmpty()) {
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不包含表达式,直接返回
|
||||||
|
if (!template.contains("${")) {
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 创建表达式上下文,支持 Map 的属性式访问
|
||||||
|
StandardEvaluationContext context = createContext(variables);
|
||||||
|
|
||||||
|
// 解析模板中的所有表达式
|
||||||
|
Matcher matcher = EXPRESSION_PATTERN.matcher(template);
|
||||||
|
StringBuffer result = new StringBuffer();
|
||||||
|
|
||||||
|
while (matcher.find()) {
|
||||||
|
String expression = matcher.group(1); // 提取 ${} 中的表达式
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 解析表达式
|
||||||
|
Expression exp = PARSER.parseExpression(expression);
|
||||||
|
Object value = exp.getValue(context);
|
||||||
|
|
||||||
|
// 替换表达式为值
|
||||||
|
String replacement = value != null ? value.toString() : "";
|
||||||
|
matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
|
||||||
|
|
||||||
|
log.debug("解析表达式: ${{{}}}} -> {}", expression, value);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("表达式解析失败: ${{{}}}, 保留原样, 错误: {}", expression, e.getMessage());
|
||||||
|
// 解析失败时保留原始表达式
|
||||||
|
matcher.appendReplacement(result, Matcher.quoteReplacement("${" + expression + "}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matcher.appendTail(result);
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("模板解析失败: {}", template, e);
|
||||||
|
return template; // 失败时返回原始模板
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析单个纯表达式(返回原始类型)
|
||||||
|
*
|
||||||
|
* <p>示例:
|
||||||
|
* <pre>{@code
|
||||||
|
* // 返回 Boolean
|
||||||
|
* Boolean result = SpelExpressionResolver.evaluateExpression(
|
||||||
|
* "deploy.status == 'SUCCESS'", variables, Boolean.class);
|
||||||
|
*
|
||||||
|
* // 返回 Integer
|
||||||
|
* Integer duration = SpelExpressionResolver.evaluateExpression(
|
||||||
|
* "deploy.duration", variables, Integer.class);
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @param expression 纯表达式(不需要 ${} 包裹)
|
||||||
|
* @param variables 上下文变量
|
||||||
|
* @param resultType 期望的返回类型
|
||||||
|
* @param <T> 返回类型
|
||||||
|
* @return 表达式计算结果
|
||||||
|
*/
|
||||||
|
public static <T> T evaluateExpression(String expression, Map<String, Object> variables, Class<T> resultType) {
|
||||||
|
if (expression == null || expression.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 创建支持 Map 属性访问的上下文
|
||||||
|
StandardEvaluationContext context = createContext(variables);
|
||||||
|
|
||||||
|
Expression exp = PARSER.parseExpression(expression);
|
||||||
|
return exp.getValue(context, resultType);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("表达式计算失败: {}", expression, e);
|
||||||
|
throw new RuntimeException("表达式计算失败: " + expression, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建支持 Map 属性式访问的 SpEL 上下文
|
||||||
|
*
|
||||||
|
* <p>关键:添加自定义 PropertyAccessor,允许像访问对象属性一样访问 Map 的 key
|
||||||
|
* 这样可以直接使用 `deploy.xxx` 语法,而不需要 `#deploy.xxx` 或 `['deploy'].xxx`
|
||||||
|
*
|
||||||
|
* @param variables 变量 Map
|
||||||
|
* @return SpEL 执行上下文
|
||||||
|
*/
|
||||||
|
private static StandardEvaluationContext createContext(Map<String, Object> variables) {
|
||||||
|
StandardEvaluationContext context = new StandardEvaluationContext(variables);
|
||||||
|
|
||||||
|
// 添加自定义的 Map 属性访问器
|
||||||
|
context.addPropertyAccessor(new MapPropertyAccessor());
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义 Map 属性访问器
|
||||||
|
*
|
||||||
|
* <p>允许像访问对象属性一样访问 Map 的 key,支持嵌套访问
|
||||||
|
*/
|
||||||
|
private static class MapPropertyAccessor implements PropertyAccessor {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<?>[] getSpecificTargetClasses() {
|
||||||
|
return new Class<?>[]{Map.class};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException {
|
||||||
|
return target instanceof Map && ((Map<?, ?>) target).containsKey(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException {
|
||||||
|
if (target instanceof Map) {
|
||||||
|
Object value = ((Map<?, ?>) target).get(name);
|
||||||
|
return new TypedValue(value);
|
||||||
|
}
|
||||||
|
return TypedValue.NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException {
|
||||||
|
return false; // 只读
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException {
|
||||||
|
throw new UnsupportedOperationException("不支持写入操作");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量解析模板字段
|
||||||
|
*
|
||||||
|
* @param templates 模板字符串数组
|
||||||
|
* @param variables 上下文变量
|
||||||
|
* @return 解析后的字符串数组
|
||||||
|
*/
|
||||||
|
public static String[] resolveAll(String[] templates, Map<String, Object> variables) {
|
||||||
|
if (templates == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] results = new String[templates.length];
|
||||||
|
for (int i = 0; i < templates.length; i++) {
|
||||||
|
results[i] = resolve(templates[i], variables);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查字符串是否包含 EL 表达式
|
||||||
|
*
|
||||||
|
* @param text 文本
|
||||||
|
* @return true 如果包含 ${...} 表达式
|
||||||
|
*/
|
||||||
|
public static boolean containsExpression(String text) {
|
||||||
|
return text != null && text.contains("${");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用方法:自动解析对象中所有包含 EL 表达式的字符串字段
|
||||||
|
*
|
||||||
|
* <p>使用反射遍历对象的所有字段,自动识别并解析包含 ${} 的字符串字段。
|
||||||
|
* 避免了硬编码字段名,支持动态扩展。
|
||||||
|
*
|
||||||
|
* <p>使用示例:
|
||||||
|
* <pre>{@code
|
||||||
|
* ApprovalTaskDTO dto = new ApprovalTaskDTO();
|
||||||
|
* dto.setTaskDescription("${deploy.applicationName}需要部署");
|
||||||
|
* dto.setApprovalTitle("部署${deploy.status == 'SUCCESS' ? '成功' : '失败'}");
|
||||||
|
*
|
||||||
|
* // 自动解析 dto 中所有包含表达式的字段
|
||||||
|
* SpelExpressionResolver.resolveObject(dto, variables);
|
||||||
|
*
|
||||||
|
* // dto 的字段已被自动解析:
|
||||||
|
* // taskDescription: "SSO管理后台需要部署"
|
||||||
|
* // approvalTitle: "部署成功"
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @param target 目标对象(会直接修改其字段值)
|
||||||
|
* @param variables 上下文变量
|
||||||
|
*/
|
||||||
|
public static void resolveObject(Object target, Map<String, Object> variables) {
|
||||||
|
if (target == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Class<?> clazz = target.getClass();
|
||||||
|
|
||||||
|
// 遍历所有字段(包括父类字段)
|
||||||
|
while (clazz != null && clazz != Object.class) {
|
||||||
|
for (java.lang.reflect.Field field : clazz.getDeclaredFields()) {
|
||||||
|
// 只处理 String 类型字段
|
||||||
|
if (field.getType() != String.class) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
field.setAccessible(true);
|
||||||
|
String value = (String) field.get(target);
|
||||||
|
|
||||||
|
// 如果字段值包含表达式,则解析
|
||||||
|
if (containsExpression(value)) {
|
||||||
|
String resolved = resolve(value, variables);
|
||||||
|
field.set(target, resolved);
|
||||||
|
log.debug("自动解析字段 {}.{}: {} -> {}",
|
||||||
|
clazz.getSimpleName(), field.getName(), value, resolved);
|
||||||
|
}
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
log.warn("无法访问字段 {}.{}: {}",
|
||||||
|
clazz.getSimpleName(), field.getName(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理父类字段
|
||||||
|
clazz = clazz.getSuperclass();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Flowable 集成方法 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析包含 EL 表达式的字符串模板(Flowable DelegateExecution 版本)
|
||||||
|
*
|
||||||
|
* <p>自动从 Flowable 执行上下文中提取变量,支持复杂表达式:
|
||||||
|
* <ul>
|
||||||
|
* <li>简单变量: <code>${deploy.applicationName}</code></li>
|
||||||
|
* <li>条件判断: <code>${deploy.status == 'SUCCESS' ? '成功' : '失败'}</code></li>
|
||||||
|
* <li>复杂运算: <code>${deploy.duration > 60 ? '耗时较长' : '正常'}</code></li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>使用示例:
|
||||||
|
* <pre>{@code
|
||||||
|
* // 在 Flowable Delegate 中使用
|
||||||
|
* String message = SpelExpressionResolver.resolve(execution,
|
||||||
|
* "应用 ${deploy.applicationName} 部署${deploy.status == 'SUCCESS' ? '成功' : '失败'}");
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @param execution Flowable 执行上下文
|
||||||
|
* @param template 模板字符串,可包含多个 ${...} 表达式
|
||||||
|
* @return 解析后的字符串
|
||||||
|
*/
|
||||||
|
public static String resolve(DelegateExecution execution, String template) {
|
||||||
|
if (execution == null || template == null) {
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 Flowable 执行上下文中获取所有变量
|
||||||
|
Map<String, Object> variables = execution.getVariables();
|
||||||
|
|
||||||
|
// 委托给通用解析方法
|
||||||
|
return resolve(template, variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析包含 EL 表达式的字符串模板(Flowable DelegateTask 版本)
|
||||||
|
*
|
||||||
|
* @param task Flowable 任务上下文
|
||||||
|
* @param template 模板字符串
|
||||||
|
* @return 解析后的字符串
|
||||||
|
*/
|
||||||
|
public static String resolve(DelegateTask task, String template) {
|
||||||
|
if (task == null || template == null) {
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 Flowable 任务上下文中获取所有变量
|
||||||
|
Map<String, Object> variables = task.getVariables();
|
||||||
|
|
||||||
|
// 委托给通用解析方法
|
||||||
|
return resolve(template, variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析单个纯表达式(返回原始类型)- Flowable DelegateExecution 版本
|
||||||
|
*
|
||||||
|
* @param execution Flowable 执行上下文
|
||||||
|
* @param expression 纯表达式(不需要 ${} 包裹)
|
||||||
|
* @param resultType 期望的返回类型
|
||||||
|
* @param <T> 返回类型
|
||||||
|
* @return 表达式计算结果
|
||||||
|
*/
|
||||||
|
public static <T> T evaluateExpression(DelegateExecution execution, String expression, Class<T> resultType) {
|
||||||
|
if (execution == null || expression == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> variables = execution.getVariables();
|
||||||
|
return evaluateExpression(expression, variables, resultType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析单个纯表达式(返回原始类型)- Flowable DelegateTask 版本
|
||||||
|
*
|
||||||
|
* @param task Flowable 任务上下文
|
||||||
|
* @param expression 纯表达式(不需要 ${} 包裹)
|
||||||
|
* @param resultType 期望的返回类型
|
||||||
|
* @param <T> 返回类型
|
||||||
|
* @return 表达式计算结果
|
||||||
|
*/
|
||||||
|
public static <T> T evaluateExpression(DelegateTask task, String expression, Class<T> resultType) {
|
||||||
|
if (task == null || expression == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> variables = task.getVariables();
|
||||||
|
return evaluateExpression(expression, variables, resultType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析表达式并返回原始类型 - 自动去除 ${} 包装(Flowable DelegateExecution 版本)
|
||||||
|
*
|
||||||
|
* <p>便捷方法,自动处理带或不带 ${} 的表达式
|
||||||
|
*
|
||||||
|
* <p>使用示例:
|
||||||
|
* <pre>{@code
|
||||||
|
* // 自动去除 ${},返回原始类型
|
||||||
|
* Object userIds = SpelExpressionResolver.evaluateAuto(execution, "${approval.userIds}", Object.class);
|
||||||
|
* // 如果是列表:userIds instanceof List = true
|
||||||
|
*
|
||||||
|
* // 也支持不带 ${} 的表达式
|
||||||
|
* Object userIds2 = SpelExpressionResolver.evaluateAuto(execution, "approval.userIds", Object.class);
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @param execution Flowable 执行上下文
|
||||||
|
* @param expression 表达式(可以带 ${} 也可以不带)
|
||||||
|
* @param resultType 期望的返回类型
|
||||||
|
* @param <T> 返回类型
|
||||||
|
* @return 表达式计算结果(原始类型)
|
||||||
|
*/
|
||||||
|
public static <T> T evaluateAuto(DelegateExecution execution, String expression, Class<T> resultType) {
|
||||||
|
if (execution == null || expression == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动去除 ${} 包装
|
||||||
|
String cleanExpression = expression.trim();
|
||||||
|
if (cleanExpression.startsWith("${") && cleanExpression.endsWith("}")) {
|
||||||
|
cleanExpression = cleanExpression.substring(2, cleanExpression.length() - 1).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return evaluateExpression(execution, cleanExpression, resultType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析表达式并返回原始类型 - 自动去除 ${} 包装(Flowable DelegateTask 版本)
|
||||||
|
*
|
||||||
|
* @param task Flowable 任务上下文
|
||||||
|
* @param expression 表达式(可以带 ${} 也可以不带)
|
||||||
|
* @param resultType 期望的返回类型
|
||||||
|
* @param <T> 返回类型
|
||||||
|
* @return 表达式计算结果(原始类型)
|
||||||
|
*/
|
||||||
|
public static <T> T evaluateAuto(DelegateTask task, String expression, Class<T> resultType) {
|
||||||
|
if (task == null || expression == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动去除 ${} 包装
|
||||||
|
String cleanExpression = expression.trim();
|
||||||
|
if (cleanExpression.startsWith("${") && cleanExpression.endsWith("}")) {
|
||||||
|
cleanExpression = cleanExpression.substring(2, cleanExpression.length() - 1).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return evaluateExpression(task, cleanExpression, resultType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用方法:自动解析对象中所有包含 EL 表达式的字符串字段 - Flowable DelegateExecution 版本
|
||||||
|
*
|
||||||
|
* @param target 目标对象(会直接修改其字段值)
|
||||||
|
* @param execution Flowable 执行上下文
|
||||||
|
*/
|
||||||
|
public static void resolveObject(Object target, DelegateExecution execution) {
|
||||||
|
if (execution == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> variables = execution.getVariables();
|
||||||
|
resolveObject(target, variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用方法:自动解析对象中所有包含 EL 表达式的字符串字段 - Flowable DelegateTask 版本
|
||||||
|
*
|
||||||
|
* @param target 目标对象(会直接修改其字段值)
|
||||||
|
* @param task Flowable 任务上下文
|
||||||
|
*/
|
||||||
|
public static void resolveObject(Object target, DelegateTask task) {
|
||||||
|
if (task == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> variables = task.getVariables();
|
||||||
|
resolveObject(target, variables);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -38,5 +38,14 @@ public interface INotificationChannelAdapter {
|
|||||||
default String validateConfig(Map<String, Object> config) {
|
default String validateConfig(Map<String, Object> config) {
|
||||||
return "配置有效";
|
return "配置有效";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试连接
|
||||||
|
* 发送一条测试消息或测试连接是否正常
|
||||||
|
*
|
||||||
|
* @param config 渠道配置
|
||||||
|
* @throws Exception 测试失败时抛出异常
|
||||||
|
*/
|
||||||
|
void testConnection(Map<String, Object> config) throws Exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -97,6 +97,65 @@ public class EmailChannelAdapter implements INotificationChannelAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void testConnection(Map<String, Object> config) throws Exception {
|
||||||
|
// 1. 解析配置
|
||||||
|
EmailNotificationConfig emailConfig = objectMapper.convertValue(config, EmailNotificationConfig.class);
|
||||||
|
|
||||||
|
// 2. 校验配置
|
||||||
|
validateEmailConfig(emailConfig);
|
||||||
|
|
||||||
|
// 3. 创建JavaMailSender
|
||||||
|
JavaMailSenderImpl mailSender = createMailSender(emailConfig);
|
||||||
|
|
||||||
|
// 4. 测试SMTP连接
|
||||||
|
log.info("测试SMTP连接 - 服务器: {}:{}", emailConfig.getSmtpHost(), emailConfig.getSmtpPort());
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 测试连接
|
||||||
|
mailSender.testConnection();
|
||||||
|
log.info("SMTP连接测试成功");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("SMTP连接测试失败: {}", e.getMessage());
|
||||||
|
throw new Exception("SMTP连接失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 发送测试邮件(可选:如果配置了默认收件人)
|
||||||
|
if (!CollectionUtils.isEmpty(emailConfig.getDefaultReceivers())) {
|
||||||
|
try {
|
||||||
|
MimeMessage mimeMessage = mailSender.createMimeMessage();
|
||||||
|
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8");
|
||||||
|
|
||||||
|
// 设置发件人
|
||||||
|
if (emailConfig.getFromName() != null && !emailConfig.getFromName().isEmpty()) {
|
||||||
|
helper.setFrom(new InternetAddress(emailConfig.getFrom(), emailConfig.getFromName(), "UTF-8"));
|
||||||
|
} else {
|
||||||
|
helper.setFrom(emailConfig.getFrom());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置收件人(使用配置的默认收件人)
|
||||||
|
helper.setTo(emailConfig.getDefaultReceivers().toArray(new String[0]));
|
||||||
|
|
||||||
|
// 设置主题
|
||||||
|
helper.setSubject("【测试邮件】Deploy Ease Platform 邮件通知测试");
|
||||||
|
|
||||||
|
// 设置内容
|
||||||
|
String content = "这是一条来自 Deploy Ease Platform 的测试邮件。\n\n" +
|
||||||
|
"如果您收到此邮件,说明邮件通知渠道配置正常!\n\n" +
|
||||||
|
"发送时间:" + java.time.LocalDateTime.now();
|
||||||
|
helper.setText(content, false);
|
||||||
|
|
||||||
|
// 发送邮件
|
||||||
|
log.info("发送测试邮件到: {}", emailConfig.getDefaultReceivers());
|
||||||
|
mailSender.send(mimeMessage);
|
||||||
|
log.info("测试邮件发送成功");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("测试邮件发送失败: {}", e.getMessage());
|
||||||
|
throw new Exception("测试邮件发送失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验邮件配置
|
* 校验邮件配置
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -120,6 +120,49 @@ public class WeworkChannelAdapter implements INotificationChannelAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void testConnection(Map<String, Object> config) throws Exception {
|
||||||
|
// 1. 解析配置
|
||||||
|
WeworkNotificationConfig weworkConfig = objectMapper.convertValue(config, WeworkNotificationConfig.class);
|
||||||
|
|
||||||
|
if (weworkConfig.getWebhookUrl() == null || weworkConfig.getWebhookUrl().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("企业微信Webhook URL未配置");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 构建测试消息
|
||||||
|
Map<String, Object> messageBody = new HashMap<>();
|
||||||
|
messageBody.put("msgtype", "text");
|
||||||
|
|
||||||
|
Map<String, Object> textContent = new HashMap<>();
|
||||||
|
textContent.put("content", "【测试消息】\n这是一条来自 Deploy Ease Platform 的测试消息,您的企业微信通知渠道配置正常!");
|
||||||
|
messageBody.put("text", textContent);
|
||||||
|
|
||||||
|
// 3. 发送测试请求
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
|
||||||
|
String jsonBody = objectMapper.writeValueAsString(messageBody);
|
||||||
|
HttpEntity<String> entity = new HttpEntity<>(jsonBody, headers);
|
||||||
|
|
||||||
|
log.info("测试企业微信连接 - URL: {}", weworkConfig.getWebhookUrl());
|
||||||
|
|
||||||
|
String response = restTemplate.exchange(
|
||||||
|
weworkConfig.getWebhookUrl(),
|
||||||
|
HttpMethod.POST,
|
||||||
|
entity,
|
||||||
|
String.class
|
||||||
|
).getBody();
|
||||||
|
|
||||||
|
log.info("企业微信连接测试成功 - 响应: {}", response);
|
||||||
|
|
||||||
|
// 4. 检查响应
|
||||||
|
if (response != null && response.contains("\"errcode\":0")) {
|
||||||
|
log.info("企业微信通知渠道测试成功");
|
||||||
|
} else {
|
||||||
|
throw new Exception("企业微信响应异常: " + response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建消息内容
|
* 构建消息内容
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -56,13 +56,32 @@ public class NotificationChannelServiceImpl
|
|||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public boolean testConnection(Long id) {
|
public boolean testConnection(Long id) {
|
||||||
|
// 1. 查询渠道配置
|
||||||
NotificationChannel channel = notificationChannelRepository.findById(id)
|
NotificationChannel channel = notificationChannelRepository.findById(id)
|
||||||
.orElseThrow(() -> new BusinessException(ResponseCode.DATA_NOT_FOUND));
|
.orElseThrow(() -> new BusinessException(ResponseCode.DATA_NOT_FOUND));
|
||||||
|
|
||||||
// TODO: 实现实际的连接测试逻辑
|
log.info("开始测试通知渠道连接: id={}, name={}, type={}",
|
||||||
log.info("测试通知渠道连接: id={}, type={}", id, channel.getChannelType());
|
id, channel.getName(), channel.getChannelType());
|
||||||
|
|
||||||
return true;
|
// 2. 获取对应的适配器
|
||||||
|
INotificationChannelAdapter adapter;
|
||||||
|
try {
|
||||||
|
adapter = adapterFactory.getAdapter(channel.getChannelType());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.error("获取通知渠道适配器失败: {}", e.getMessage());
|
||||||
|
throw new BusinessException(ResponseCode.ERROR, new Object[]{"不支持的渠道类型: " + channel.getChannelType()});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 执行连接测试
|
||||||
|
try {
|
||||||
|
adapter.testConnection(channel.getConfig());
|
||||||
|
log.info("通知渠道连接测试成功: id={}, name={}", id, channel.getName());
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("通知渠道连接测试失败: id={}, name={}, 错误: {}",
|
||||||
|
id, channel.getName(), e.getMessage(), e);
|
||||||
|
throw new BusinessException(ResponseCode.ERROR, new Object[]{"连接测试失败: " + e.getMessage()});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -9,6 +9,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@ -29,23 +32,23 @@ public class WorkflowCategoryApiController extends BaseController<WorkflowCatego
|
|||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response<WorkflowCategoryDTO> create(WorkflowCategoryDTO dto) {
|
public Response<WorkflowCategoryDTO> create(@Validated @RequestBody WorkflowCategoryDTO dto) {
|
||||||
return super.create(dto);
|
return super.create(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response<WorkflowCategoryDTO> update(Long aLong, WorkflowCategoryDTO dto) {
|
public Response<WorkflowCategoryDTO> update(@PathVariable Long id, @Validated @RequestBody WorkflowCategoryDTO dto) {
|
||||||
return super.update(aLong, dto);
|
return super.update(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response<Void> delete(Long aLong) {
|
public Response<Void> delete(@PathVariable Long id) {
|
||||||
return super.delete(aLong);
|
return super.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response<WorkflowCategoryDTO> findById(Long aLong) {
|
public Response<WorkflowCategoryDTO> findById(@PathVariable Long id) {
|
||||||
return super.findById(aLong);
|
return super.findById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.delegate;
|
package com.qqchen.deploy.backend.workflow.delegate;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.utils.SpelExpressionResolver;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.inputmapping.ApprovalInputMapping;
|
import com.qqchen.deploy.backend.workflow.dto.inputmapping.ApprovalInputMapping;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.outputs.ApprovalOutputs;
|
import com.qqchen.deploy.backend.workflow.dto.outputs.ApprovalOutputs;
|
||||||
import com.qqchen.deploy.backend.workflow.enums.ApprovalModeEnum;
|
import com.qqchen.deploy.backend.workflow.enums.ApprovalModeEnum;
|
||||||
@ -112,9 +113,7 @@ public class ApprovalTaskListener extends BaseTaskListener<ApprovalInputMapping,
|
|||||||
String approverVariableExpression = input.getApproverVariable();
|
String approverVariableExpression = input.getApproverVariable();
|
||||||
log.info("解析审批人变量表达式: {}", approverVariableExpression);
|
log.info("解析审批人变量表达式: {}", approverVariableExpression);
|
||||||
|
|
||||||
// 使用工具类直接解析表达式(支持 ${approval.userIds} 或 approval.userIds)
|
Object approverValue = SpelExpressionResolver.evaluateAuto(task, approverVariableExpression, Object.class);
|
||||||
Object approverValue = com.qqchen.deploy.backend.framework.utils.NestedMapUtils
|
|
||||||
.resolveExpression(task, approverVariableExpression);
|
|
||||||
|
|
||||||
if (approverValue != null) {
|
if (approverValue != null) {
|
||||||
log.info("解析得到审批人: {}", approverValue);
|
log.info("解析得到审批人: {}", approverValue);
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package com.qqchen.deploy.backend.workflow.delegate;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.qqchen.deploy.backend.framework.utils.NestedMapUtils;
|
import com.qqchen.deploy.backend.framework.utils.SpelExpressionResolver;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.outputs.BaseNodeOutputs;
|
import com.qqchen.deploy.backend.workflow.dto.outputs.BaseNodeOutputs;
|
||||||
import com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum;
|
import com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum;
|
||||||
import com.qqchen.deploy.backend.workflow.model.NodeContext;
|
import com.qqchen.deploy.backend.workflow.model.NodeContext;
|
||||||
@ -188,8 +188,8 @@ public abstract class BaseNodeDelegate<I, O> implements JavaDelegate {
|
|||||||
if (value instanceof String) {
|
if (value instanceof String) {
|
||||||
String strValue = (String) value;
|
String strValue = (String) value;
|
||||||
try {
|
try {
|
||||||
// ✅ 使用统一的 resolve 方法,自动智能处理所有情况
|
// ✅ 使用 SpelExpressionResolver 统一解析方法,支持复杂表达式
|
||||||
Object resolvedValue = NestedMapUtils.resolve(execution, strValue);
|
String resolvedValue = SpelExpressionResolver.resolve(execution, strValue);
|
||||||
log.debug("解析字段: {} = {} -> {}", entry.getKey(), strValue, resolvedValue);
|
log.debug("解析字段: {} = {} -> {}", entry.getKey(), strValue, resolvedValue);
|
||||||
resolvedMap.put(entry.getKey(), resolvedValue);
|
resolvedMap.put(entry.getKey(), resolvedValue);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@ -2,7 +2,9 @@ package com.qqchen.deploy.backend.workflow.listener.flowable.execution;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.qqchen.deploy.backend.framework.utils.SpelExpressionResolver;
|
||||||
import com.qqchen.deploy.backend.workflow.constants.WorkFlowConstants;
|
import com.qqchen.deploy.backend.workflow.constants.WorkFlowConstants;
|
||||||
|
import com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum;
|
||||||
import com.qqchen.deploy.backend.workflow.enums.WorkflowNodeInstanceStatusEnums;
|
import com.qqchen.deploy.backend.workflow.enums.WorkflowNodeInstanceStatusEnums;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.event.WorkflowNodeInstanceStatusChangeEvent;
|
import com.qqchen.deploy.backend.workflow.dto.event.WorkflowNodeInstanceStatusChangeEvent;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@ -23,6 +25,22 @@ import java.util.*;
|
|||||||
@Component("globalNodeExecutionListener")
|
@Component("globalNodeExecutionListener")
|
||||||
public class GlobalNodeExecutionListener implements ExecutionListener {
|
public class GlobalNodeExecutionListener implements ExecutionListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 不产生 outputs 的节点类型(事件/网关节点)
|
||||||
|
* 这些节点执行后不会设置 outputs.status,直接视为成功
|
||||||
|
*/
|
||||||
|
private static final Set<String> NODES_WITHOUT_OUTPUTS = Set.of(
|
||||||
|
"StartEvent", // 开始事件
|
||||||
|
"EndEvent", // 结束事件
|
||||||
|
"BoundaryEvent", // 边界事件
|
||||||
|
"IntermediateCatchEvent", // 中间捕获事件
|
||||||
|
"IntermediateThrowEvent", // 中间抛出事件
|
||||||
|
"ExclusiveGateway", // 排他网关
|
||||||
|
"ParallelGateway", // 并行网关
|
||||||
|
"InclusiveGateway", // 包容网关
|
||||||
|
"EventBasedGateway" // 事件网关
|
||||||
|
);
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
@Lazy
|
@Lazy
|
||||||
private ApplicationEventPublisher eventPublisher;
|
private ApplicationEventPublisher eventPublisher;
|
||||||
@ -55,21 +73,17 @@ public class GlobalNodeExecutionListener implements ExecutionListener {
|
|||||||
break;
|
break;
|
||||||
case ExecutionListener.EVENTNAME_END:
|
case ExecutionListener.EVENTNAME_END:
|
||||||
// 从节点的 outputs.status 读取执行状态
|
// 从节点的 outputs.status 读取执行状态
|
||||||
com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum nodeExecutionStatus = getNodeOutputStatus(execution, nodeId);
|
NodeExecutionStatusEnum nodeExecutionStatus = getNodeOutputStatus(execution, nodeId, nodeType);
|
||||||
|
if (nodeExecutionStatus == NodeExecutionStatusEnum.FAILURE) {
|
||||||
if (nodeExecutionStatus == com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum.FAILURE) {
|
|
||||||
status = WorkflowNodeInstanceStatusEnums.FAILED;
|
status = WorkflowNodeInstanceStatusEnums.FAILED;
|
||||||
|
// ✅ 失败时提取错误信息
|
||||||
|
errorMessage = extractErrorMessage(execution, nodeId);
|
||||||
} else {
|
} else {
|
||||||
status = WorkflowNodeInstanceStatusEnums.COMPLETED;
|
status = WorkflowNodeInstanceStatusEnums.COMPLETED;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 收集节点执行变量
|
// ✅ 收集节点执行变量
|
||||||
variablesJson = collectNodeVariables(execution, nodeId, status);
|
variablesJson = collectNodeVariables(execution, nodeId);
|
||||||
|
|
||||||
// ✅ 如果失败,提取错误信息
|
|
||||||
if (status == WorkflowNodeInstanceStatusEnums.FAILED) {
|
|
||||||
errorMessage = extractErrorMessage(execution, nodeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
endTime = now;
|
endTime = now;
|
||||||
break;
|
break;
|
||||||
@ -95,23 +109,31 @@ public class GlobalNodeExecutionListener implements ExecutionListener {
|
|||||||
* 从节点的 outputs.status 获取执行状态
|
* 从节点的 outputs.status 获取执行状态
|
||||||
*
|
*
|
||||||
* @param execution Flowable执行上下文
|
* @param execution Flowable执行上下文
|
||||||
* @param nodeId 节点ID
|
* @param nodeId 节点ID
|
||||||
* @return 节点执行状态枚举,如果没有则返回 null
|
* @param nodeType 节点类型(ServiceTask/StartEvent/Gateway等)
|
||||||
|
* @return 节点执行状态枚举
|
||||||
*/
|
*/
|
||||||
private com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum getNodeOutputStatus(DelegateExecution execution, String nodeId) {
|
private NodeExecutionStatusEnum getNodeOutputStatus(DelegateExecution execution, String nodeId, String nodeType) {
|
||||||
|
// ✅ 第一层防护:事件/网关节点不产生 outputs,直接返回成功
|
||||||
|
if (NODES_WITHOUT_OUTPUTS.contains(nodeType)) {
|
||||||
|
log.debug("节点 {} 类型为 {},不产生 outputs,直接返回 SUCCESS", nodeId, nodeType);
|
||||||
|
return NodeExecutionStatusEnum.SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 第二层:执行型节点,查找 outputs.status
|
||||||
try {
|
try {
|
||||||
// 使用工具类从节点变量中获取 outputs.status
|
// 使用 SpelExpressionResolver 从节点变量中获取 outputs.status(支持复杂表达式)
|
||||||
Object statusObj = com.qqchen.deploy.backend.framework.utils.NestedMapUtils
|
String expression = nodeId + ".outputs.status";
|
||||||
.getValueFromExecution(execution, nodeId + ".outputs.status");
|
Object statusObj = SpelExpressionResolver.evaluateExpression(execution, expression, Object.class);
|
||||||
|
|
||||||
if (statusObj != null) {
|
if (statusObj != null) {
|
||||||
// 如果已经是枚举类型,直接返回
|
// 如果已经是枚举类型,直接返回
|
||||||
if (statusObj instanceof com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum) {
|
if (statusObj instanceof NodeExecutionStatusEnum) {
|
||||||
return (com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum) statusObj;
|
return (NodeExecutionStatusEnum) statusObj;
|
||||||
}
|
}
|
||||||
// 如果是字符串,转换为枚举
|
// 如果是字符串,转换为枚举
|
||||||
try {
|
try {
|
||||||
return com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum.valueOf(statusObj.toString());
|
return NodeExecutionStatusEnum.valueOf(statusObj.toString());
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
log.warn("无法将 {} 转换为 NodeExecutionStatusEnum", statusObj);
|
log.warn("无法将 {} 转换为 NodeExecutionStatusEnum", statusObj);
|
||||||
}
|
}
|
||||||
@ -119,19 +141,19 @@ public class GlobalNodeExecutionListener implements ExecutionListener {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.debug("无法获取节点 {} 的 outputs.status: {}", nodeId, e.getMessage());
|
log.debug("无法获取节点 {} 的 outputs.status: {}", nodeId, e.getMessage());
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
// ✅ 其他情况:默认失败(后续可优化为严格模式)
|
||||||
|
return NodeExecutionStatusEnum.FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 收集节点执行变量
|
* 收集节点执行变量
|
||||||
*
|
*
|
||||||
* @param execution Flowable执行上下文
|
* @param execution Flowable执行上下文
|
||||||
* @param nodeId 节点ID
|
* @param nodeId 节点ID
|
||||||
* @param status 节点执行状态
|
|
||||||
* @return JSON格式的变量字符串
|
* @return JSON格式的变量字符串
|
||||||
*/
|
*/
|
||||||
private String collectNodeVariables(DelegateExecution execution, String nodeId,
|
private String collectNodeVariables(DelegateExecution execution, String nodeId) {
|
||||||
WorkflowNodeInstanceStatusEnums status) {
|
|
||||||
try {
|
try {
|
||||||
// ✅ 直接获取所有流程变量
|
// ✅ 直接获取所有流程变量
|
||||||
Map<String, Object> allVariables = execution.getVariables();
|
Map<String, Object> allVariables = execution.getVariables();
|
||||||
@ -152,7 +174,7 @@ public class GlobalNodeExecutionListener implements ExecutionListener {
|
|||||||
* 提取错误信息
|
* 提取错误信息
|
||||||
*
|
*
|
||||||
* @param execution Flowable执行上下文
|
* @param execution Flowable执行上下文
|
||||||
* @param nodeId 节点ID
|
* @param nodeId 节点ID
|
||||||
* @return 错误信息字符串
|
* @return 错误信息字符串
|
||||||
*/
|
*/
|
||||||
private String extractErrorMessage(DelegateExecution execution, String nodeId) {
|
private String extractErrorMessage(DelegateExecution execution, String nodeId) {
|
||||||
@ -161,9 +183,9 @@ public class GlobalNodeExecutionListener implements ExecutionListener {
|
|||||||
if (nodeOutput instanceof Map) {
|
if (nodeOutput instanceof Map) {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Map<String, Object> outputMap = (Map<String, Object>) nodeOutput;
|
Map<String, Object> outputMap = (Map<String, Object>) nodeOutput;
|
||||||
Object errorMsg = outputMap.get("errorMessage");
|
Object message = outputMap.get("message");
|
||||||
if (errorMsg != null) {
|
if (message != null) {
|
||||||
return errorMsg.toString();
|
return message.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package com.qqchen.deploy.backend.workflow.service.impl;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.qqchen.deploy.backend.framework.exception.BusinessException;
|
import com.qqchen.deploy.backend.framework.exception.BusinessException;
|
||||||
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
||||||
|
import com.qqchen.deploy.backend.framework.utils.SpelExpressionResolver;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.inputmapping.ApprovalInputMapping;
|
import com.qqchen.deploy.backend.workflow.dto.inputmapping.ApprovalInputMapping;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.outputs.ApprovalOutputs;
|
import com.qqchen.deploy.backend.workflow.dto.outputs.ApprovalOutputs;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.request.ApprovalTaskRequest;
|
import com.qqchen.deploy.backend.workflow.dto.request.ApprovalTaskRequest;
|
||||||
@ -178,6 +179,11 @@ public class ApprovalTaskServiceImpl implements IApprovalTaskService {
|
|||||||
dto.setRequireComment((Boolean) variables.get("requireComment"));
|
dto.setRequireComment((Boolean) variables.get("requireComment"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ 通用解析:自动解析 DTO 中所有包含 EL 表达式的字符串字段
|
||||||
|
// 无需硬编码字段名,支持未来扩展
|
||||||
|
// 会自动处理:taskDescription, approvalTitle, approvalContent 等所有 String 字段
|
||||||
|
SpelExpressionResolver.resolveObject(dto, variables);
|
||||||
|
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -75,6 +75,8 @@ VALUES
|
|||||||
(203, '定时任务管理', '/deploy/schedule-jobs', 'Deploy/ScheduleJob/List', 'ClockCircleOutlined', 'deploy:schedule-job', 2, 200, 3, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
(203, '定时任务管理', '/deploy/schedule-jobs', 'Deploy/ScheduleJob/List', 'ClockCircleOutlined', 'deploy:schedule-job', 2, 200, 3, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
||||||
-- 环境管理
|
-- 环境管理
|
||||||
(204, '环境管理', '/deploy/environments', 'Deploy/Environment/List', 'CloudOutlined', 'deploy:environment', 2, 200, 4, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
(204, '环境管理', '/deploy/environments', 'Deploy/Environment/List', 'CloudOutlined', 'deploy:environment', 2, 200, 4, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
||||||
|
-- 消息中心
|
||||||
|
(205, '消息中心', '/deploy/notification-channels', 'Deploy/NotificationChannel/List', 'BellOutlined', 'deploy:notification-channel', 2, 200, 5, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
||||||
|
|
||||||
-- 资源管理
|
-- 资源管理
|
||||||
(300, '资源管理', '/resource', NULL, 'DatabaseOutlined', NULL, 1, NULL, 3, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
(300, '资源管理', '/resource', NULL, 'DatabaseOutlined', NULL, 1, NULL, 3, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
||||||
@ -140,7 +142,7 @@ SELECT 1, id FROM sys_menu; -- 管理员拥有所有菜单权限
|
|||||||
|
|
||||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||||
VALUES
|
VALUES
|
||||||
(2, 200), (2, 201), (2, 202), (2, 203), (2, 204), (2, 300), (2, 301), (2, 302), (2, 303), (2, 304), -- 运维拥有运维管理和资源管理权限
|
(2, 200), (2, 201), (2, 202), (2, 203), (2, 204), (2, 205), (2, 300), (2, 301), (2, 302), (2, 303), (2, 304), -- 运维拥有运维管理和资源管理权限
|
||||||
(3, 100), (3, 101), (3, 102), (3, 104), (3, 200), (3, 202); -- 开发拥有工作流和应用管理权限
|
(3, 100), (3, 101), (3, 102), (3, 104), (3, 200), (3, 202); -- 开发拥有工作流和应用管理权限
|
||||||
|
|
||||||
-- ==================== 初始化权限数据 ====================
|
-- ==================== 初始化权限数据 ====================
|
||||||
@ -222,6 +224,17 @@ DELETE FROM sys_permission WHERE id < 10000;
|
|||||||
-- (144, NOW(), 204, 'deploy:environment:update', '环境修改', 'FUNCTION', 4),
|
-- (144, NOW(), 204, 'deploy:environment:update', '环境修改', 'FUNCTION', 4),
|
||||||
-- (145, NOW(), 204, 'deploy:environment:delete', '环境删除', 'FUNCTION', 5),
|
-- (145, NOW(), 204, 'deploy:environment:delete', '环境删除', 'FUNCTION', 5),
|
||||||
--
|
--
|
||||||
|
-- -- 消息中心 (menu_id=205)
|
||||||
|
-- (146, NOW(), 205, 'deploy:notification-channel:list', '通知渠道查询', 'FUNCTION', 1),
|
||||||
|
-- (147, NOW(), 205, 'deploy:notification-channel:view', '通知渠道详情', 'FUNCTION', 2),
|
||||||
|
-- (148, NOW(), 205, 'deploy:notification-channel:create', '通知渠道创建', 'FUNCTION', 3),
|
||||||
|
-- (149, NOW(), 205, 'deploy:notification-channel:update', '通知渠道修改', 'FUNCTION', 4),
|
||||||
|
-- (150, NOW(), 205, 'deploy:notification-channel:delete', '通知渠道删除', 'FUNCTION', 5),
|
||||||
|
-- (151, NOW(), 205, 'deploy:notification-channel:test', '测试连接', 'FUNCTION', 6),
|
||||||
|
-- (152, NOW(), 205, 'deploy:notification-channel:enable', '启用渠道', 'FUNCTION', 7),
|
||||||
|
-- (153, NOW(), 205, 'deploy:notification-channel:disable', '禁用渠道', 'FUNCTION', 8),
|
||||||
|
-- (154, NOW(), 205, 'deploy:notification-channel:send', '发送通知', 'FUNCTION', 9),
|
||||||
|
--
|
||||||
-- -- 团队配置管理 (无对应菜单,menu_id=NULL)
|
-- -- 团队配置管理 (无对应菜单,menu_id=NULL)
|
||||||
-- (151, NOW(), NULL, 'deploy:team-config:list', '团队配置查询', 'FUNCTION', 11),
|
-- (151, NOW(), NULL, 'deploy:team-config:list', '团队配置查询', 'FUNCTION', 11),
|
||||||
-- (152, NOW(), NULL, 'deploy:team-config:view', '团队配置详情', 'FUNCTION', 12),
|
-- (152, NOW(), NULL, 'deploy:team-config:view', '团队配置详情', 'FUNCTION', 12),
|
||||||
|
|||||||
@ -0,0 +1,374 @@
|
|||||||
|
package com.qqchen.deploy.backend.framework.utils;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring EL 表达式解析工具测试
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @date 2025-11-04
|
||||||
|
*/
|
||||||
|
class SpelExpressionResolverTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试用的 DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
static class TestDTO {
|
||||||
|
private String taskDescription;
|
||||||
|
private String approvalTitle;
|
||||||
|
private String approvalContent;
|
||||||
|
private String statusMessage;
|
||||||
|
private Integer version; // 非字符串字段,不应被解析
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试:简单变量引用
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testSimpleVariableReference() {
|
||||||
|
Map<String, Object> variables = new HashMap<>();
|
||||||
|
Map<String, Object> deploy = new HashMap<>();
|
||||||
|
deploy.put("applicationName", "SSO管理后台");
|
||||||
|
variables.put("deploy", deploy);
|
||||||
|
|
||||||
|
String template = "${deploy.applicationName}需要部署";
|
||||||
|
String result = SpelExpressionResolver.resolve(template, variables);
|
||||||
|
|
||||||
|
assertEquals("SSO管理后台需要部署", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试:条件判断(三元运算符)
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testConditionalExpression() {
|
||||||
|
Map<String, Object> variables = new HashMap<>();
|
||||||
|
Map<String, Object> deploy = new HashMap<>();
|
||||||
|
deploy.put("status", "SUCCESS");
|
||||||
|
variables.put("deploy", deploy);
|
||||||
|
|
||||||
|
String template = "部署${deploy.status == 'SUCCESS' ? '成功' : '失败'}";
|
||||||
|
String result = SpelExpressionResolver.resolve(template, variables);
|
||||||
|
|
||||||
|
assertEquals("部署成功", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试:复杂条件判断
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testComplexConditionalExpression() {
|
||||||
|
Map<String, Object> variables = new HashMap<>();
|
||||||
|
Map<String, Object> deploy = new HashMap<>();
|
||||||
|
deploy.put("duration", 120);
|
||||||
|
variables.put("deploy", deploy);
|
||||||
|
|
||||||
|
String template = "部署耗时${deploy.duration > 60 ? '较长(' + deploy.duration + '秒)' : '正常'}";
|
||||||
|
String result = SpelExpressionResolver.resolve(template, variables);
|
||||||
|
|
||||||
|
assertEquals("部署耗时较长(120秒)", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试:多表达式混合
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testMultipleExpressions() {
|
||||||
|
Map<String, Object> variables = new HashMap<>();
|
||||||
|
Map<String, Object> deploy = new HashMap<>();
|
||||||
|
deploy.put("applicationName", "SSO管理后台");
|
||||||
|
deploy.put("environment", "生产环境");
|
||||||
|
deploy.put("status", "SUCCESS");
|
||||||
|
variables.put("deploy", deploy);
|
||||||
|
|
||||||
|
String template = "【部署通知】应用 ${deploy.applicationName} 在 ${deploy.environment} 部署${deploy.status == 'SUCCESS' ? '成功' : '失败'}";
|
||||||
|
String result = SpelExpressionResolver.resolve(template, variables);
|
||||||
|
|
||||||
|
assertEquals("【部署通知】应用 SSO管理后台 在 生产环境 部署成功", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试:嵌套对象访问
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testNestedObjectAccess() {
|
||||||
|
Map<String, Object> variables = new HashMap<>();
|
||||||
|
Map<String, Object> user = new HashMap<>();
|
||||||
|
Map<String, Object> department = new HashMap<>();
|
||||||
|
department.put("name", "研发部");
|
||||||
|
user.put("name", "张三");
|
||||||
|
user.put("department", department);
|
||||||
|
variables.put("user", user);
|
||||||
|
|
||||||
|
String template = "用户 ${user.name} 来自 ${user.department.name}";
|
||||||
|
String result = SpelExpressionResolver.resolve(template, variables);
|
||||||
|
|
||||||
|
assertEquals("用户 张三 来自 研发部", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试:数学运算
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testMathematicalExpression() {
|
||||||
|
Map<String, Object> variables = new HashMap<>();
|
||||||
|
variables.put("count", 5);
|
||||||
|
variables.put("price", 10.5);
|
||||||
|
|
||||||
|
String template = "总价:${count * price} 元";
|
||||||
|
String result = SpelExpressionResolver.resolve(template, variables);
|
||||||
|
|
||||||
|
assertEquals("总价:52.5 元", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试:布尔逻辑运算
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testBooleanLogic() {
|
||||||
|
Map<String, Object> variables = new HashMap<>();
|
||||||
|
Map<String, Object> deploy = new HashMap<>();
|
||||||
|
deploy.put("status", "SUCCESS");
|
||||||
|
deploy.put("requireApproval", true);
|
||||||
|
variables.put("deploy", deploy);
|
||||||
|
|
||||||
|
String template = "${deploy.status == 'SUCCESS' && deploy.requireApproval ? '审批通过并部署成功' : '等待审批或部署失败'}";
|
||||||
|
String result = SpelExpressionResolver.resolve(template, variables);
|
||||||
|
|
||||||
|
assertEquals("审批通过并部署成功", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试:evaluateExpression - 返回原始类型
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testEvaluateExpression() {
|
||||||
|
Map<String, Object> variables = new HashMap<>();
|
||||||
|
Map<String, Object> deploy = new HashMap<>();
|
||||||
|
deploy.put("status", "SUCCESS");
|
||||||
|
deploy.put("duration", 120);
|
||||||
|
variables.put("deploy", deploy);
|
||||||
|
|
||||||
|
// 返回布尔值
|
||||||
|
Boolean isSuccess = SpelExpressionResolver.evaluateExpression(
|
||||||
|
"deploy.status == 'SUCCESS'", variables, Boolean.class);
|
||||||
|
assertEquals(true, isSuccess);
|
||||||
|
|
||||||
|
// 返回整数
|
||||||
|
Integer duration = SpelExpressionResolver.evaluateExpression(
|
||||||
|
"deploy.duration", variables, Integer.class);
|
||||||
|
assertEquals(120, duration);
|
||||||
|
|
||||||
|
// 返回字符串
|
||||||
|
String message = SpelExpressionResolver.evaluateExpression(
|
||||||
|
"deploy.status == 'SUCCESS' ? '成功' : '失败'", variables, String.class);
|
||||||
|
assertEquals("成功", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试:resolveObject - 自动解析对象中的所有字段(核心功能)
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testResolveObject() {
|
||||||
|
// 准备测试数据
|
||||||
|
Map<String, Object> variables = new HashMap<>();
|
||||||
|
Map<String, Object> deploy = new HashMap<>();
|
||||||
|
deploy.put("applicationName", "SSO管理后台");
|
||||||
|
deploy.put("environment", "生产环境");
|
||||||
|
deploy.put("status", "SUCCESS");
|
||||||
|
deploy.put("duration", 150);
|
||||||
|
variables.put("deploy", deploy);
|
||||||
|
|
||||||
|
// 创建包含表达式的 DTO
|
||||||
|
TestDTO dto = new TestDTO();
|
||||||
|
dto.setTaskDescription("${deploy.applicationName}需要部署到${deploy.environment}");
|
||||||
|
dto.setApprovalTitle("部署审批");
|
||||||
|
dto.setApprovalContent("应用 ${deploy.applicationName} 部署${deploy.status == 'SUCCESS' ? '成功' : '失败'}");
|
||||||
|
dto.setStatusMessage("耗时${deploy.duration > 60 ? '较长' : '正常'}");
|
||||||
|
dto.setVersion(1); // 非字符串字段
|
||||||
|
|
||||||
|
// ✅ 自动解析所有包含表达式的字符串字段
|
||||||
|
SpelExpressionResolver.resolveObject(dto, variables);
|
||||||
|
|
||||||
|
// 验证结果
|
||||||
|
assertEquals("SSO管理后台需要部署到生产环境", dto.getTaskDescription());
|
||||||
|
assertEquals("部署审批", dto.getApprovalTitle()); // 无表达式,保持原样
|
||||||
|
assertEquals("应用 SSO管理后台 部署成功", dto.getApprovalContent());
|
||||||
|
assertEquals("耗时较长", dto.getStatusMessage());
|
||||||
|
assertEquals(1, dto.getVersion()); // 非字符串字段不受影响
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试:表达式解析失败时的处理
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testExpressionResolutionFailure() {
|
||||||
|
Map<String, Object> variables = new HashMap<>();
|
||||||
|
variables.put("deploy", new HashMap<>());
|
||||||
|
|
||||||
|
// 访问不存在的字段
|
||||||
|
String template = "应用 ${deploy.nonExistentField} 部署完成";
|
||||||
|
String result = SpelExpressionResolver.resolve(template, variables);
|
||||||
|
|
||||||
|
// 应该保留原样或返回空值
|
||||||
|
assertNotNull(result);
|
||||||
|
System.out.println("解析失败时的结果: " + result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试:无表达式的字符串
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testNoExpression() {
|
||||||
|
Map<String, Object> variables = new HashMap<>();
|
||||||
|
|
||||||
|
String template = "这是一个普通字符串,没有表达式";
|
||||||
|
String result = SpelExpressionResolver.resolve(template, variables);
|
||||||
|
|
||||||
|
assertEquals("这是一个普通字符串,没有表达式", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试:null 值处理
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testNullHandling() {
|
||||||
|
String result = SpelExpressionResolver.resolve(null, new HashMap<>());
|
||||||
|
assertEquals(null, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试:批量解析
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testResolveAll() {
|
||||||
|
Map<String, Object> variables = new HashMap<>();
|
||||||
|
Map<String, Object> deploy = new HashMap<>();
|
||||||
|
deploy.put("applicationName", "SSO管理后台");
|
||||||
|
deploy.put("status", "SUCCESS");
|
||||||
|
variables.put("deploy", deploy);
|
||||||
|
|
||||||
|
String[] templates = {
|
||||||
|
"${deploy.applicationName}需要部署",
|
||||||
|
"部署${deploy.status == 'SUCCESS' ? '成功' : '失败'}",
|
||||||
|
"普通文本"
|
||||||
|
};
|
||||||
|
|
||||||
|
String[] results = SpelExpressionResolver.resolveAll(templates, variables);
|
||||||
|
|
||||||
|
assertEquals(3, results.length);
|
||||||
|
assertEquals("SSO管理后台需要部署", results[0]);
|
||||||
|
assertEquals("部署成功", results[1]);
|
||||||
|
assertEquals("普通文本", results[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试:集合访问(数组/列表索引)
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testCollectionAccess() {
|
||||||
|
Map<String, Object> variables = new HashMap<>();
|
||||||
|
|
||||||
|
// 购物车示例
|
||||||
|
Map<String, Object> shoppingCart = new HashMap<>();
|
||||||
|
java.util.List<Map<String, Object>> items = new java.util.ArrayList<>();
|
||||||
|
|
||||||
|
Map<String, Object> item1 = new HashMap<>();
|
||||||
|
item1.put("name", "iPhone 15 Pro");
|
||||||
|
item1.put("price", 7999);
|
||||||
|
item1.put("quantity", 1);
|
||||||
|
|
||||||
|
Map<String, Object> item2 = new HashMap<>();
|
||||||
|
item2.put("name", "MacBook Pro");
|
||||||
|
item2.put("price", 14999);
|
||||||
|
item2.put("quantity", 2);
|
||||||
|
|
||||||
|
items.add(item1);
|
||||||
|
items.add(item2);
|
||||||
|
shoppingCart.put("items", items);
|
||||||
|
shoppingCart.put("total", 3);
|
||||||
|
variables.put("shoppingCart", shoppingCart);
|
||||||
|
|
||||||
|
// 测试列表索引访问
|
||||||
|
String template1 = "第一个商品: ${shoppingCart.items[0].name}";
|
||||||
|
String result1 = SpelExpressionResolver.resolve(template1, variables);
|
||||||
|
assertEquals("第一个商品: iPhone 15 Pro", result1);
|
||||||
|
|
||||||
|
// 测试嵌套访问和运算
|
||||||
|
String template2 = "第二个商品 ${shoppingCart.items[1].name} 单价 ${shoppingCart.items[1].price} 元,数量 ${shoppingCart.items[1].quantity} 件";
|
||||||
|
String result2 = SpelExpressionResolver.resolve(template2, variables);
|
||||||
|
assertEquals("第二个商品 MacBook Pro 单价 14999 元,数量 2 件", result2);
|
||||||
|
|
||||||
|
// 测试列表大小访问
|
||||||
|
String template3 = "购物车共有 ${shoppingCart.items.size()} 件商品";
|
||||||
|
String result3 = SpelExpressionResolver.resolve(template3, variables);
|
||||||
|
assertEquals("购物车共有 2 件商品", result3);
|
||||||
|
|
||||||
|
// 测试复杂表达式:索引 + 条件判断
|
||||||
|
String template4 = "第一个商品价格${shoppingCart.items[0].price > 5000 ? '较高' : '合理'}";
|
||||||
|
String result4 = SpelExpressionResolver.resolve(template4, variables);
|
||||||
|
assertEquals("第一个商品价格较高", result4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试:数组访问
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testArrayAccess() {
|
||||||
|
Map<String, Object> variables = new HashMap<>();
|
||||||
|
|
||||||
|
String[] fruits = {"苹果", "香蕉", "橙子"};
|
||||||
|
variables.put("fruits", fruits);
|
||||||
|
|
||||||
|
int[] numbers = {10, 20, 30, 40, 50};
|
||||||
|
variables.put("numbers", numbers);
|
||||||
|
|
||||||
|
// 字符串数组访问
|
||||||
|
String template1 = "第一个水果: ${fruits[0]},第三个水果: ${fruits[2]}";
|
||||||
|
String result1 = SpelExpressionResolver.resolve(template1, variables);
|
||||||
|
assertEquals("第一个水果: 苹果,第三个水果: 橙子", result1);
|
||||||
|
|
||||||
|
// 数字数组访问和运算
|
||||||
|
String template2 = "第二个数字是 ${numbers[1]},总和是 ${numbers[0] + numbers[1] + numbers[2]}";
|
||||||
|
String result2 = SpelExpressionResolver.resolve(template2, variables);
|
||||||
|
assertEquals("第二个数字是 20,总和是 60", result2);
|
||||||
|
|
||||||
|
// 数组长度访问
|
||||||
|
String template3 = "水果数组长度: ${fruits.length},数字数组长度: ${numbers.length}";
|
||||||
|
String result3 = SpelExpressionResolver.resolve(template3, variables);
|
||||||
|
assertEquals("水果数组长度: 3,数字数组长度: 5", result3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试:Map 键访问(使用方括号语法)
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testMapKeyAccess() {
|
||||||
|
Map<String, Object> variables = new HashMap<>();
|
||||||
|
|
||||||
|
Map<String, Object> config = new HashMap<>();
|
||||||
|
config.put("max-connections", 100);
|
||||||
|
config.put("timeout-seconds", 30);
|
||||||
|
config.put("enable-ssl", true);
|
||||||
|
variables.put("config", config);
|
||||||
|
|
||||||
|
// Map 键访问(带特殊字符的键需要使用方括号)
|
||||||
|
String template1 = "最大连接数: ${config['max-connections']}";
|
||||||
|
String result1 = SpelExpressionResolver.resolve(template1, variables);
|
||||||
|
assertEquals("最大连接数: 100", result1);
|
||||||
|
|
||||||
|
// 多个 Map 键访问
|
||||||
|
String template2 = "超时时间 ${config['timeout-seconds']} 秒,SSL${config['enable-ssl'] ? '已启用' : '未启用'}";
|
||||||
|
String result2 = SpelExpressionResolver.resolve(template2, variables);
|
||||||
|
assertEquals("超时时间 30 秒,SSL已启用", result2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user