打印了JENKINS节点日志

This commit is contained in:
dengqichen 2025-11-04 18:17:36 +08:00
parent 211c00f930
commit 2f2637763c
13 changed files with 1102 additions and 612 deletions

View File

@ -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.statusa.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.userIdssid_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();
}
}

View File

@ -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);
}
}

View File

@ -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;
} }

View File

@ -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);
}
}
}
/** /**
* 校验邮件配置 * 校验邮件配置
*/ */

View File

@ -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);
}
}
/** /**
* 构建消息内容 * 构建消息内容
*/ */

View File

@ -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

View File

@ -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

View File

@ -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;
@ -16,7 +17,7 @@ import java.util.Map;
/** /**
* 审批任务监听器 * 审批任务监听器
* UserTask 创建时被调用负责配置审批人和任务基本信息 * UserTask 创建时被调用负责配置审批人和任务基本信息
* *
* @author qqchen * @author qqchen
* @since 2025-10-23 * @since 2025-10-23
*/ */
@ -31,10 +32,10 @@ public class ApprovalTaskListener extends BaseTaskListener<ApprovalInputMapping,
ApprovalInputMapping inputMapping ApprovalInputMapping inputMapping
) { ) {
log.info("Configuring approval task: {}", delegateTask.getId()); log.info("Configuring approval task: {}", delegateTask.getId());
// 1. 配置审批人 // 1. 配置审批人
configureTaskAssignee(delegateTask, inputMapping); configureTaskAssignee(delegateTask, inputMapping);
// 2. 设置任务基本信息 // 2. 设置任务基本信息
if (configs.get("nodeName") != null) { if (configs.get("nodeName") != null) {
delegateTask.setName((String) configs.get("nodeName")); delegateTask.setName((String) configs.get("nodeName"));
@ -42,7 +43,7 @@ public class ApprovalTaskListener extends BaseTaskListener<ApprovalInputMapping,
if (inputMapping.getApprovalTitle() != null) { if (inputMapping.getApprovalTitle() != null) {
delegateTask.setDescription(inputMapping.getApprovalTitle()); delegateTask.setDescription(inputMapping.getApprovalTitle());
} }
// 3. 设置超时时间 // 3. 设置超时时间
if (inputMapping.getTimeoutDuration() != null && inputMapping.getTimeoutDuration() > 0) { if (inputMapping.getTimeoutDuration() != null && inputMapping.getTimeoutDuration() > 0) {
java.util.Date dueDate = java.sql.Timestamp.valueOf( java.util.Date dueDate = java.sql.Timestamp.valueOf(
@ -50,10 +51,10 @@ public class ApprovalTaskListener extends BaseTaskListener<ApprovalInputMapping,
); );
delegateTask.setDueDate(dueDate); delegateTask.setDueDate(dueDate);
} }
// 4. 保存审批信息到任务局部变量供前端查询 // 4. 保存审批信息到任务局部变量供前端查询
saveApprovalInfoToTaskVariables(delegateTask, inputMapping); saveApprovalInfoToTaskVariables(delegateTask, inputMapping);
log.info("Approval task configured - assignee: {}", delegateTask.getAssignee()); log.info("Approval task configured - assignee: {}", delegateTask.getAssignee());
} }
@ -66,7 +67,7 @@ public class ApprovalTaskListener extends BaseTaskListener<ApprovalInputMapping,
log.warn("ApproverType is null, skip setting assignee"); log.warn("ApproverType is null, skip setting assignee");
return; return;
} }
switch (input.getApproverType()) { switch (input.getApproverType()) {
case USER: case USER:
// 指定用户审批 // 指定用户审批
@ -85,7 +86,7 @@ public class ApprovalTaskListener extends BaseTaskListener<ApprovalInputMapping,
} }
} }
break; break;
case ROLE: case ROLE:
// 指定角色审批设置候选组 // 指定角色审批设置候选组
if (input.getApproverRoles() != null && !input.getApproverRoles().isEmpty()) { if (input.getApproverRoles() != null && !input.getApproverRoles().isEmpty()) {
@ -95,7 +96,7 @@ public class ApprovalTaskListener extends BaseTaskListener<ApprovalInputMapping,
log.info("Set candidate groups (roles): {}", input.getApproverRoles()); log.info("Set candidate groups (roles): {}", input.getApproverRoles());
} }
break; break;
case DEPARTMENT: case DEPARTMENT:
// 指定部门审批设置候选组 // 指定部门审批设置候选组
if (input.getApproverDepartments() != null && !input.getApproverDepartments().isEmpty()) { if (input.getApproverDepartments() != null && !input.getApproverDepartments().isEmpty()) {
@ -105,20 +106,18 @@ public class ApprovalTaskListener extends BaseTaskListener<ApprovalInputMapping,
log.info("Set candidate groups (departments): {}", input.getApproverDepartments()); log.info("Set candidate groups (departments): {}", input.getApproverDepartments());
} }
break; break;
case VARIABLE: case VARIABLE:
// 从流程变量中获取审批人 // 从流程变量中获取审批人
if (input.getApproverVariable() != null) { if (input.getApproverVariable() != null) {
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);
// 处理不同类型的审批人数据 // 处理不同类型的审批人数据
if (approverValue instanceof List) { if (approverValue instanceof List) {
// 列表形式可能是多个审批人 // 列表形式可能是多个审批人
@ -161,12 +160,12 @@ public class ApprovalTaskListener extends BaseTaskListener<ApprovalInputMapping,
if (input.getApprovalContent() != null) { if (input.getApprovalContent() != null) {
delegateTask.setVariableLocal("approvalContent", input.getApprovalContent()); delegateTask.setVariableLocal("approvalContent", input.getApprovalContent());
} }
// 保存审批模式 // 保存审批模式
if (input.getApprovalMode() != null) { if (input.getApprovalMode() != null) {
delegateTask.setVariableLocal("approvalMode", input.getApprovalMode().getCode()); delegateTask.setVariableLocal("approvalMode", input.getApprovalMode().getCode());
} }
// 保存审批配置 // 保存审批配置
if (input.getAllowDelegate() != null) { if (input.getAllowDelegate() != null) {
delegateTask.setVariableLocal("allowDelegate", input.getAllowDelegate()); delegateTask.setVariableLocal("allowDelegate", input.getAllowDelegate());
@ -177,14 +176,14 @@ public class ApprovalTaskListener extends BaseTaskListener<ApprovalInputMapping,
if (input.getRequireComment() != null) { if (input.getRequireComment() != null) {
delegateTask.setVariableLocal("requireComment", input.getRequireComment()); delegateTask.setVariableLocal("requireComment", input.getRequireComment());
} }
// 保存候选组如果有 // 保存候选组如果有
if (input.getApproverRoles() != null && !input.getApproverRoles().isEmpty()) { if (input.getApproverRoles() != null && !input.getApproverRoles().isEmpty()) {
delegateTask.setVariableLocal("candidateGroups", String.join(",", input.getApproverRoles())); delegateTask.setVariableLocal("candidateGroups", String.join(",", input.getApproverRoles()));
} else if (input.getApproverDepartments() != null && !input.getApproverDepartments().isEmpty()) { } else if (input.getApproverDepartments() != null && !input.getApproverDepartments().isEmpty()) {
delegateTask.setVariableLocal("candidateGroups", String.join(",", input.getApproverDepartments())); delegateTask.setVariableLocal("candidateGroups", String.join(",", input.getApproverDepartments()));
} }
log.debug("Saved approval info to task variables for taskId: {}", delegateTask.getId()); log.debug("Saved approval info to task variables for taskId: {}", delegateTask.getId());
} }
} }

View File

@ -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) {

View File

@ -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;
@ -47,7 +65,7 @@ public class GlobalNodeExecutionListener implements ExecutionListener {
LocalDateTime endTime = null; LocalDateTime endTime = null;
String variablesJson = null; String variablesJson = null;
String errorMessage = null; String errorMessage = null;
switch (eventName) { switch (eventName) {
case ExecutionListener.EVENTNAME_START: case ExecutionListener.EVENTNAME_START:
status = WorkflowNodeInstanceStatusEnums.RUNNING; status = WorkflowNodeInstanceStatusEnums.RUNNING;
@ -55,22 +73,18 @@ 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;
default: default:
@ -93,25 +107,33 @@ 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,29 +141,29 @@ 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();
if (allVariables == null || allVariables.isEmpty()) { if (allVariables == null || allVariables.isEmpty()) {
return null; return null;
} }
return objectMapper.writeValueAsString(allVariables); return objectMapper.writeValueAsString(allVariables);
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to collect node variables for nodeId: {}", nodeId, e); log.error("Failed to collect node variables for nodeId: {}", nodeId, e);
return null; return null;
@ -150,9 +172,9 @@ 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) {

View File

@ -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;
} }
} }

View File

@ -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),

View File

@ -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);
}
}