From 2fd852c5c671057909a47ab568882677d1a650d8 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Thu, 20 Nov 2025 15:11:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(puzzle):=20=E5=A2=9E=E5=BC=BA=E6=8B=BC?= =?UTF-8?q?=E5=9B=BE=E5=A1=AB=E5=85=85=E5=BC=95=E6=93=8E=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 requireRuleMatch 参数控制是否必须匹配规则 - 重构 DeviceCountConditionStrategy 支持两种匹配模式 - 移除已废弃的 DeviceCountRangeConditionStrategy - 引入 FillResult 类封装填充结果信息 - 优化条件上下文和数据源上下文的 extra 字段类型 - 更新相关测试用例和文档说明 --- src/main/java/com/ycwl/basic/puzzle/claude.md | 4 +- .../puzzle/dto/PuzzleGenerateRequest.java | 7 + .../ycwl/basic/puzzle/fill/FillResult.java | 66 ++++++ .../puzzle/fill/PuzzleElementFillEngine.java | 23 ++- .../fill/condition/ConditionContext.java | 3 +- .../DeviceCountConditionStrategy.java | 122 ++++++++++- .../DeviceCountRangeConditionStrategy.java | 42 ---- .../fill/datasource/DataSourceContext.java | 4 +- .../DeviceImageDataSourceStrategy.java | 41 +++- .../puzzle/fill/enums/ConditionType.java | 5 - .../impl/PuzzleGenerateServiceImpl.java | 29 ++- .../DeviceCountConditionStrategyTest.java | 195 ++++++++++++++++++ ...DeviceCountRangeConditionStrategyTest.java | 190 ----------------- 13 files changed, 470 insertions(+), 261 deletions(-) create mode 100644 src/main/java/com/ycwl/basic/puzzle/fill/FillResult.java delete mode 100644 src/main/java/com/ycwl/basic/puzzle/fill/condition/DeviceCountRangeConditionStrategy.java delete mode 100644 src/test/java/com/ycwl/basic/puzzle/fill/condition/DeviceCountRangeConditionStrategyTest.java diff --git a/src/main/java/com/ycwl/basic/puzzle/claude.md b/src/main/java/com/ycwl/basic/puzzle/claude.md index ee8bdef5..5f946c53 100644 --- a/src/main/java/com/ycwl/basic/puzzle/claude.md +++ b/src/main/java/com/ycwl/basic/puzzle/claude.md @@ -259,8 +259,8 @@ Map execute(Long templateId, Long faceId, Long scenicId) | 策略类型 | 类名 | 匹配逻辑 | 配置示例 | |---------|------|---------|---------| | 总是匹配 | AlwaysConditionStrategy | 总是返回true,用作兜底规则 | `{}` | -| 机位数量匹配 | DeviceCountConditionStrategy | 精确匹配机位数量 | `{"deviceCount": 4}` | -| 机位数量范围 | DeviceCountRangeConditionStrategy | 机位数量在指定范围内 | `{"minCount": 2, "maxCount": 5}` | +| 机位数量匹配(模式1) | DeviceCountConditionStrategy | 精确匹配所有机位的数量 | `{"deviceCount": 4}` | +| 机位数量匹配(模式2) | DeviceCountConditionStrategy | 从指定列表中过滤并匹配数量,保持配置顺序 | `{"deviceCount": 2, "deviceIds": [200, 300, 400]}` | | 机位ID匹配 | DeviceIdMatchConditionStrategy | 匹配指定的机位ID(支持ANY/ALL模式) | `{"deviceIds": [200, 300], "matchMode": "ALL"}` | **数据源类型**: diff --git a/src/main/java/com/ycwl/basic/puzzle/dto/PuzzleGenerateRequest.java b/src/main/java/com/ycwl/basic/puzzle/dto/PuzzleGenerateRequest.java index edb83fab..17413a0a 100644 --- a/src/main/java/com/ycwl/basic/puzzle/dto/PuzzleGenerateRequest.java +++ b/src/main/java/com/ycwl/basic/puzzle/dto/PuzzleGenerateRequest.java @@ -56,4 +56,11 @@ public class PuzzleGenerateRequest { * 仅对JPEG格式有效 */ private Integer quality; + + /** + * 是否必须匹配填充规则才能生成(可选,默认false) + * true: 如果没有规则匹配,抛出异常,不生成图片 + * false: 无论是否匹配规则,都继续生成(默认行为) + */ + private Boolean requireRuleMatch = false; } diff --git a/src/main/java/com/ycwl/basic/puzzle/fill/FillResult.java b/src/main/java/com/ycwl/basic/puzzle/fill/FillResult.java new file mode 100644 index 00000000..8683a246 --- /dev/null +++ b/src/main/java/com/ycwl/basic/puzzle/fill/FillResult.java @@ -0,0 +1,66 @@ +package com.ycwl.basic.puzzle.fill; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.HashMap; +import java.util.Map; + +/** + * 拼图元素填充结果 + * + * @author Claude + * @since 2025-01-20 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FillResult { + + /** + * 是否匹配到规则 + */ + private boolean ruleMatched; + + /** + * 匹配的规则名称(如果有) + */ + private String matchedRuleName; + + /** + * 填充的数据 + */ + @Builder.Default + private Map dynamicData = new HashMap<>(); + + /** + * 成功填充的元素数量 + */ + private int filledCount; + + /** + * 创建空结果(未匹配) + */ + public static FillResult noMatch() { + return FillResult.builder() + .ruleMatched(false) + .dynamicData(new HashMap<>()) + .filledCount(0) + .build(); + } + + /** + * 创建匹配成功的结果 + */ + public static FillResult matched(String ruleName, Map data, int count) { + return FillResult.builder() + .ruleMatched(true) + .matchedRuleName(ruleName) + .dynamicData(data) + .filledCount(count) + .build(); + } +} diff --git a/src/main/java/com/ycwl/basic/puzzle/fill/PuzzleElementFillEngine.java b/src/main/java/com/ycwl/basic/puzzle/fill/PuzzleElementFillEngine.java index e990f432..2f514d52 100644 --- a/src/main/java/com/ycwl/basic/puzzle/fill/PuzzleElementFillEngine.java +++ b/src/main/java/com/ycwl/basic/puzzle/fill/PuzzleElementFillEngine.java @@ -50,14 +50,12 @@ public class PuzzleElementFillEngine { * @param templateId 模板ID * @param faceId 人脸ID * @param scenicId 景区ID - * @return 填充后的dynamicData + * @return 填充结果(包含是否匹配规则的信息) */ - public Map execute(Long templateId, Long faceId, Long scenicId) { - Map dynamicData = new HashMap<>(); - + public FillResult execute(Long templateId, Long faceId, Long scenicId) { if (faceId == null) { log.debug("自动填充被跳过, templateId={}, faceId={}", templateId, faceId); - return dynamicData; + return FillResult.noMatch(); } try { @@ -65,7 +63,7 @@ public class PuzzleElementFillEngine { List rules = ruleMapper.listByTemplateId(templateId); if (rules == null || rules.isEmpty()) { log.debug("模板[{}]没有配置自动填充规则", templateId); - return dynamicData; + return FillResult.noMatch(); } log.info("模板[{}]共有{}条填充规则,开始执行...", templateId, rules.size()); @@ -105,8 +103,10 @@ public class PuzzleElementFillEngine { DataSourceContext dataSourceContext = DataSourceContext.builder() .faceId(faceId) .scenicId(scenicId) + .extra(conditionContext.getExtra()) .build(); + Map dynamicData = new HashMap<>(); int successCount = 0; for (PuzzleFillRuleItemEntity item : items) { String value = dataSourceResolver.resolve( @@ -128,14 +128,17 @@ public class PuzzleElementFillEngine { log.info("规则[{}]执行完成,成功填充{}/{}个元素", rule.getRuleName(), successCount, items.size()); - // 6. 匹配第一条后停止 - break; + // 6. 返回匹配成功的结果 + return FillResult.matched(rule.getRuleName(), dynamicData, successCount); } + // 所有规则都不匹配 + log.info("所有规则都不匹配, templateId={}, faceId={}", templateId, faceId); + return FillResult.noMatch(); + } catch (Exception e) { log.error("自动填充引擎执行异常, templateId={}, faceId={}", templateId, faceId, e); + return FillResult.noMatch(); } - - return dynamicData; } } diff --git a/src/main/java/com/ycwl/basic/puzzle/fill/condition/ConditionContext.java b/src/main/java/com/ycwl/basic/puzzle/fill/condition/ConditionContext.java index 42d1d524..7bb39c47 100644 --- a/src/main/java/com/ycwl/basic/puzzle/fill/condition/ConditionContext.java +++ b/src/main/java/com/ycwl/basic/puzzle/fill/condition/ConditionContext.java @@ -4,6 +4,7 @@ import lombok.Builder; import lombok.Data; import java.util.List; +import java.util.Map; /** * 条件评估上下文 @@ -31,5 +32,5 @@ public class ConditionContext { /** * 可扩展的其他上下文数据 */ - private Object extra; + private Map extra; } diff --git a/src/main/java/com/ycwl/basic/puzzle/fill/condition/DeviceCountConditionStrategy.java b/src/main/java/com/ycwl/basic/puzzle/fill/condition/DeviceCountConditionStrategy.java index 918a0905..36503225 100644 --- a/src/main/java/com/ycwl/basic/puzzle/fill/condition/DeviceCountConditionStrategy.java +++ b/src/main/java/com/ycwl/basic/puzzle/fill/condition/DeviceCountConditionStrategy.java @@ -2,28 +2,142 @@ package com.ycwl.basic.puzzle.fill.condition; import com.fasterxml.jackson.databind.JsonNode; import com.ycwl.basic.puzzle.fill.enums.ConditionType; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + /** * 机位数量精确匹配策略 + * + *

支持两种匹配模式:

+ *
    + *
  • 模式1:全局数量匹配 - 只指定 deviceCount,匹配所有机位的数量
  • + *
  • 模式2:指定列表数量匹配 - 同时指定 deviceCount + deviceIds,从指定列表中过滤并匹配数量
  • + *
+ * + *

配置示例:

+ *
+ * // 模式1:全局数量匹配
+ * {
+ *   "deviceCount": 4
+ * }
+ *
+ * // 模式2:指定列表数量匹配
+ * {
+ *   "deviceCount": 2,
+ *   "deviceIds": [200, 300, 400]
+ * }
+ * 
+ * + *

模式2匹配逻辑:

+ *
    + *
  • 从 deviceIds 列表中过滤出实际存在的机位
  • + *
  • 保持配置顺序(不按 deviceId 排序)
  • + *
  • 判断过滤后的数量是否等于 deviceCount
  • + *
  • 匹配成功后,将过滤后的机位列表存入 context.extra,供数据源解析使用
  • + *
+ * + * @author Claude + * @since 2025-01-20 */ +@Slf4j @Component public class DeviceCountConditionStrategy implements ConditionStrategy { @Override public boolean evaluate(JsonNode conditionValue, ConditionContext context) { if (conditionValue == null || !conditionValue.has("deviceCount")) { + log.warn("DEVICE_COUNT条件缺少deviceCount字段"); return false; } int expectedCount = conditionValue.get("deviceCount").asInt(); - Integer actualCount = context.getDeviceCount(); - - if (actualCount == null) { + if (expectedCount <= 0) { + log.warn("deviceCount必须大于0, 当前值: {}", expectedCount); return false; } - return actualCount == expectedCount; + // 检查是否指定了 deviceIds(模式2) + if (conditionValue.has("deviceIds")) { + return evaluateWithDeviceIdList(conditionValue, context, expectedCount); + } else { + // 模式1:全局数量匹配 + return evaluateGlobalCount(context, expectedCount); + } + } + + /** + * 模式1:全局数量匹配 + */ + private boolean evaluateGlobalCount(ConditionContext context, int expectedCount) { + Integer actualCount = context.getDeviceCount(); + if (actualCount == null) { + log.debug("上下文中没有机位数量信息"); + return false; + } + + boolean matched = actualCount == expectedCount; + if (matched) { + log.info("DEVICE_COUNT全局匹配成功: 期望数量={}, 实际数量={}", expectedCount, actualCount); + } else { + log.debug("DEVICE_COUNT全局匹配失败: 期望数量={}, 实际数量={}", expectedCount, actualCount); + } + return matched; + } + + /** + * 模式2:指定列表数量匹配 + */ + private boolean evaluateWithDeviceIdList(JsonNode conditionValue, ConditionContext context, int expectedCount) { + // 1. 读取配置的 deviceIds 列表 + JsonNode deviceIdsNode = conditionValue.get("deviceIds"); + if (!deviceIdsNode.isArray()) { + log.warn("deviceIds字段必须是数组"); + return false; + } + + List requiredDeviceIds = new ArrayList<>(); + deviceIdsNode.forEach(node -> requiredDeviceIds.add(node.asLong())); + + if (requiredDeviceIds.isEmpty()) { + log.warn("deviceIds数组为空"); + return false; + } + + // 2. 获取上下文中的机位列表 + List contextDeviceIds = context.getDeviceIds(); + if (contextDeviceIds == null || contextDeviceIds.isEmpty()) { + log.debug("上下文中没有机位ID列表"); + return false; + } + + // 3. 按配置顺序过滤出实际存在的机位 + List matchedDeviceIds = requiredDeviceIds.stream() + .filter(contextDeviceIds::contains) + .collect(Collectors.toList()); // 保持配置顺序,不排序 + + // 4. 精确匹配数量 + boolean matched = matchedDeviceIds.size() == expectedCount; + + if (matched) { + // 5. 将过滤后的机位列表存入 context.extra + Map extra = new HashMap<>(); + extra.put("filteredDeviceIds", matchedDeviceIds); + context.setExtra(extra); + + log.info("DEVICE_COUNT列表匹配成功: 配置列表={}, 过滤后={}, 期望数量={}, 实际数量={}", + requiredDeviceIds, matchedDeviceIds, expectedCount, matchedDeviceIds.size()); + } else { + log.debug("DEVICE_COUNT列表匹配失败: 配置列表={}, 过滤后={}, 期望数量={}, 实际数量={}", + requiredDeviceIds, matchedDeviceIds, expectedCount, matchedDeviceIds.size()); + } + + return matched; } @Override diff --git a/src/main/java/com/ycwl/basic/puzzle/fill/condition/DeviceCountRangeConditionStrategy.java b/src/main/java/com/ycwl/basic/puzzle/fill/condition/DeviceCountRangeConditionStrategy.java deleted file mode 100644 index c770b1dc..00000000 --- a/src/main/java/com/ycwl/basic/puzzle/fill/condition/DeviceCountRangeConditionStrategy.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.ycwl.basic.puzzle.fill.condition; - -import com.fasterxml.jackson.databind.JsonNode; -import com.ycwl.basic.puzzle.fill.enums.ConditionType; -import org.springframework.stereotype.Component; - -/** - * 机位数量范围匹配策略 - */ -@Component -public class DeviceCountRangeConditionStrategy implements ConditionStrategy { - - @Override - public boolean evaluate(JsonNode conditionValue, ConditionContext context) { - if (conditionValue == null) { - return false; - } - - Integer actualCount = context.getDeviceCount(); - if (actualCount == null) { - return false; - } - - Integer minCount = conditionValue.has("minCount") ? conditionValue.get("minCount").asInt() : null; - Integer maxCount = conditionValue.has("maxCount") ? conditionValue.get("maxCount").asInt() : null; - - if (minCount != null && actualCount < minCount) { - return false; - } - - if (maxCount != null && actualCount > maxCount) { - return false; - } - - return true; - } - - @Override - public String getSupportedType() { - return ConditionType.DEVICE_COUNT_RANGE.getCode(); - } -} diff --git a/src/main/java/com/ycwl/basic/puzzle/fill/datasource/DataSourceContext.java b/src/main/java/com/ycwl/basic/puzzle/fill/datasource/DataSourceContext.java index aeffe286..f9069482 100644 --- a/src/main/java/com/ycwl/basic/puzzle/fill/datasource/DataSourceContext.java +++ b/src/main/java/com/ycwl/basic/puzzle/fill/datasource/DataSourceContext.java @@ -3,6 +3,8 @@ package com.ycwl.basic.puzzle.fill.datasource; import lombok.Builder; import lombok.Data; +import java.util.Map; + /** * 数据源解析上下文 */ @@ -23,5 +25,5 @@ public class DataSourceContext { /** * 可扩展的其他上下文数据 */ - private Object extra; + private Map extra; } diff --git a/src/main/java/com/ycwl/basic/puzzle/fill/datasource/DeviceImageDataSourceStrategy.java b/src/main/java/com/ycwl/basic/puzzle/fill/datasource/DeviceImageDataSourceStrategy.java index 86cd7560..0dec17b8 100644 --- a/src/main/java/com/ycwl/basic/puzzle/fill/datasource/DeviceImageDataSourceStrategy.java +++ b/src/main/java/com/ycwl/basic/puzzle/fill/datasource/DeviceImageDataSourceStrategy.java @@ -8,6 +8,9 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import java.util.List; +import java.util.Map; + /** * 设备图片数据源策略 * 根据deviceIndex指定第N个设备的图片 @@ -39,6 +42,42 @@ public class DeviceImageDataSourceStrategy implements DataSourceStrategy { sortStrategy = "LATEST"; } + // 1. 检查是否有过滤后的机位列表 + Map extra = context.getExtra(); + if (extra != null && extra.containsKey("filteredDeviceIds")) { + @SuppressWarnings("unchecked") + List filteredDeviceIds = (List) extra.get("filteredDeviceIds"); + + if (filteredDeviceIds != null && !filteredDeviceIds.isEmpty()) { + // 使用过滤后的机位列表 + if (deviceIndex >= filteredDeviceIds.size()) { + log.warn("deviceIndex[{}]超出过滤后的机位列表范围, 最大索引={}", + deviceIndex, filteredDeviceIds.size() - 1); + return null; + } + + Long targetDeviceId = filteredDeviceIds.get(deviceIndex); + log.debug("使用过滤后的机位列表, deviceIndex={}, targetDeviceId={}", + deviceIndex, targetDeviceId); + + SourceEntity source = sourceMapper.getSourceByFaceAndDeviceId( + context.getFaceId(), + targetDeviceId, + type, + sortStrategy + ); + + if (source != null) { + String url = type == 1 ? source.getVideoUrl() : source.getUrl(); + log.debug("解析DEVICE_IMAGE成功(过滤模式), faceId={}, deviceId={}, type={}, url={}", + context.getFaceId(), targetDeviceId, type, url); + return url; + } + return null; + } + } + + // 2. 降级到原有逻辑(使用deviceIndex直接查询) SourceEntity source = sourceMapper.getSourceByFaceAndDeviceIndex( context.getFaceId(), deviceIndex, @@ -48,7 +87,7 @@ public class DeviceImageDataSourceStrategy implements DataSourceStrategy { if (source != null) { String url = type == 1 ? source.getVideoUrl() : source.getUrl(); - log.debug("解析DEVICE_IMAGE成功, faceId={}, deviceIndex={}, type={}, url={}", + log.debug("解析DEVICE_IMAGE成功(索引模式), faceId={}, deviceIndex={}, type={}, url={}", context.getFaceId(), deviceIndex, type, url); return url; } diff --git a/src/main/java/com/ycwl/basic/puzzle/fill/enums/ConditionType.java b/src/main/java/com/ycwl/basic/puzzle/fill/enums/ConditionType.java index a6857f36..bc3be077 100644 --- a/src/main/java/com/ycwl/basic/puzzle/fill/enums/ConditionType.java +++ b/src/main/java/com/ycwl/basic/puzzle/fill/enums/ConditionType.java @@ -13,11 +13,6 @@ public enum ConditionType { */ DEVICE_COUNT("DEVICE_COUNT", "机位数量匹配"), - /** - * 机位数量范围 - */ - DEVICE_COUNT_RANGE("DEVICE_COUNT_RANGE", "机位数量范围"), - /** * 机位ID精确匹配 */ diff --git a/src/main/java/com/ycwl/basic/puzzle/service/impl/PuzzleGenerateServiceImpl.java b/src/main/java/com/ycwl/basic/puzzle/service/impl/PuzzleGenerateServiceImpl.java index f6dd31e4..001ea9fc 100644 --- a/src/main/java/com/ycwl/basic/puzzle/service/impl/PuzzleGenerateServiceImpl.java +++ b/src/main/java/com/ycwl/basic/puzzle/service/impl/PuzzleGenerateServiceImpl.java @@ -8,6 +8,7 @@ import com.ycwl.basic.puzzle.dto.PuzzleGenerateResponse; import com.ycwl.basic.puzzle.entity.PuzzleElementEntity; import com.ycwl.basic.puzzle.entity.PuzzleGenerationRecordEntity; import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity; +import com.ycwl.basic.puzzle.fill.FillResult; import com.ycwl.basic.puzzle.fill.PuzzleElementFillEngine; import com.ycwl.basic.puzzle.mapper.PuzzleElementMapper; import com.ycwl.basic.puzzle.mapper.PuzzleGenerationRecordMapper; @@ -229,16 +230,25 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService { } // 1. 自动填充(基于faceId和规则) + boolean ruleMatched = false; if (request.getFaceId() != null) { try { - Map autoFilled = fillEngine.execute( + FillResult fillResult = fillEngine.execute( template.getId(), request.getFaceId(), scenicId ); - if (autoFilled != null && !autoFilled.isEmpty()) { - dynamicData.putAll(autoFilled); - log.info("自动填充成功, 填充了{}个元素", autoFilled.size()); + + ruleMatched = fillResult.isRuleMatched(); + + if (fillResult.isRuleMatched()) { + log.info("自动填充成功: 匹配规则[{}], 填充了{}个元素", + fillResult.getMatchedRuleName(), + fillResult.getFilledCount()); + dynamicData.putAll(fillResult.getDynamicData()); + } else { + log.info("自动填充未匹配任何规则, templateId={}, faceId={}", + template.getId(), request.getFaceId()); } } catch (Exception e) { log.error("自动填充异常, templateId={}, faceId={}", template.getId(), request.getFaceId(), e); @@ -246,7 +256,16 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService { } } - // 2. 手动数据覆盖(优先级更高) + // 2. 检查是否必须匹配规则 + Boolean requireRuleMatch = request.getRequireRuleMatch(); + if (Boolean.TRUE.equals(requireRuleMatch) && !ruleMatched) { + throw new IllegalArgumentException( + String.format("未匹配到任何填充规则,无法生成图片 (templateCode=%s, faceId=%s, requireRuleMatch=true)", + request.getTemplateCode(), request.getFaceId()) + ); + } + + // 3. 手动数据覆盖(优先级更高) if (request.getDynamicData() != null && !request.getDynamicData().isEmpty()) { dynamicData.putAll(request.getDynamicData()); log.debug("合并手动传入的dynamicData, count={}", request.getDynamicData().size()); diff --git a/src/test/java/com/ycwl/basic/puzzle/fill/condition/DeviceCountConditionStrategyTest.java b/src/test/java/com/ycwl/basic/puzzle/fill/condition/DeviceCountConditionStrategyTest.java index 65c435e0..22cee08c 100644 --- a/src/test/java/com/ycwl/basic/puzzle/fill/condition/DeviceCountConditionStrategyTest.java +++ b/src/test/java/com/ycwl/basic/puzzle/fill/condition/DeviceCountConditionStrategyTest.java @@ -6,6 +6,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + import static org.junit.jupiter.api.Assertions.*; /** @@ -130,7 +134,198 @@ class DeviceCountConditionStrategyTest { // When boolean result = strategy.evaluate(conditionValue, context); + // Then + assertFalse(result); // 修改:deviceCount必须大于0 + } + + // ==================== 模式2:指定列表数量匹配 ==================== + + @Test + @DisplayName("模式2:当指定列表中恰好有期望数量的机位时应该返回true") + void shouldReturnTrueWhenDeviceIdListMatchesCount() throws Exception { + // Given + String conditionValueJson = "{\"deviceCount\": 2, \"deviceIds\": [200, 300, 400]}"; + JsonNode conditionValue = objectMapper.readTree(conditionValueJson); + + List contextDeviceIds = Arrays.asList(200L, 300L, 500L); + ConditionContext context = ConditionContext.builder() + .deviceCount(3) + .deviceIds(contextDeviceIds) + .build(); + + // When + boolean result = strategy.evaluate(conditionValue, context); + // Then assertTrue(result); + assertNotNull(context.getExtra()); + assertTrue(context.getExtra().containsKey("filteredDeviceIds")); + + @SuppressWarnings("unchecked") + List filteredIds = (List) context.getExtra().get("filteredDeviceIds"); + assertEquals(2, filteredIds.size()); + assertEquals(Arrays.asList(200L, 300L), filteredIds); // 保持配置顺序 + } + + @Test + @DisplayName("模式2:当指定列表中的机位数量不足时应该返回false") + void shouldReturnFalseWhenDeviceIdListCountInsufficient() throws Exception { + // Given + String conditionValueJson = "{\"deviceCount\": 3, \"deviceIds\": [200, 300, 400]}"; + JsonNode conditionValue = objectMapper.readTree(conditionValueJson); + + List contextDeviceIds = Arrays.asList(200L, 300L, 500L); + ConditionContext context = ConditionContext.builder() + .deviceCount(3) + .deviceIds(contextDeviceIds) + .build(); + + // When + boolean result = strategy.evaluate(conditionValue, context); + + // Then + assertFalse(result); + } + + @Test + @DisplayName("模式2:当指定列表中的机位数量超过期望时应该返回false") + void shouldReturnFalseWhenDeviceIdListCountExcessive() throws Exception { + // Given + String conditionValueJson = "{\"deviceCount\": 1, \"deviceIds\": [200, 300, 400]}"; + JsonNode conditionValue = objectMapper.readTree(conditionValueJson); + + List contextDeviceIds = Arrays.asList(200L, 300L, 500L); + ConditionContext context = ConditionContext.builder() + .deviceCount(3) + .deviceIds(contextDeviceIds) + .build(); + + // When + boolean result = strategy.evaluate(conditionValue, context); + + // Then + assertFalse(result); + } + + @Test + @DisplayName("模式2:应该保持配置的机位顺序,不排序") + void shouldPreserveDeviceIdOrderInMode2() throws Exception { + // Given - 配置顺序为 400, 200, 300 + String conditionValueJson = "{\"deviceCount\": 3, \"deviceIds\": [400, 200, 300]}"; + JsonNode conditionValue = objectMapper.readTree(conditionValueJson); + + List contextDeviceIds = Arrays.asList(200L, 300L, 400L); + ConditionContext context = ConditionContext.builder() + .deviceCount(3) + .deviceIds(contextDeviceIds) + .build(); + + // When + boolean result = strategy.evaluate(conditionValue, context); + + // Then + assertTrue(result); + + @SuppressWarnings("unchecked") + List filteredIds = (List) context.getExtra().get("filteredDeviceIds"); + assertEquals(Arrays.asList(400L, 200L, 300L), filteredIds); // 保持配置顺序,不是[200, 300, 400] + } + + @Test + @DisplayName("模式2:当deviceIds为空数组时应该返回false") + void shouldReturnFalseWhenDeviceIdsIsEmptyArray() throws Exception { + // Given + String conditionValueJson = "{\"deviceCount\": 2, \"deviceIds\": []}"; + JsonNode conditionValue = objectMapper.readTree(conditionValueJson); + + List contextDeviceIds = Arrays.asList(200L, 300L); + ConditionContext context = ConditionContext.builder() + .deviceCount(2) + .deviceIds(contextDeviceIds) + .build(); + + // When + boolean result = strategy.evaluate(conditionValue, context); + + // Then + assertFalse(result); + } + + @Test + @DisplayName("模式2:当deviceIds不是数组类型时应该返回false") + void shouldReturnFalseWhenDeviceIdsIsNotArray() throws Exception { + // Given + String conditionValueJson = "{\"deviceCount\": 2, \"deviceIds\": \"not-an-array\"}"; + JsonNode conditionValue = objectMapper.readTree(conditionValueJson); + + List contextDeviceIds = Arrays.asList(200L, 300L); + ConditionContext context = ConditionContext.builder() + .deviceCount(2) + .deviceIds(contextDeviceIds) + .build(); + + // When + boolean result = strategy.evaluate(conditionValue, context); + + // Then + assertFalse(result); + } + + @Test + @DisplayName("模式2:当context中没有deviceIds时应该返回false") + void shouldReturnFalseWhenContextDeviceIdsIsNull() throws Exception { + // Given + String conditionValueJson = "{\"deviceCount\": 2, \"deviceIds\": [200, 300]}"; + JsonNode conditionValue = objectMapper.readTree(conditionValueJson); + + ConditionContext context = ConditionContext.builder() + .deviceCount(2) + .deviceIds(null) + .build(); + + // When + boolean result = strategy.evaluate(conditionValue, context); + + // Then + assertFalse(result); + } + + @Test + @DisplayName("模式2:当context中deviceIds为空列表时应该返回false") + void shouldReturnFalseWhenContextDeviceIdsIsEmpty() throws Exception { + // Given + String conditionValueJson = "{\"deviceCount\": 2, \"deviceIds\": [200, 300]}"; + JsonNode conditionValue = objectMapper.readTree(conditionValueJson); + + ConditionContext context = ConditionContext.builder() + .deviceCount(2) + .deviceIds(Collections.emptyList()) + .build(); + + // When + boolean result = strategy.evaluate(conditionValue, context); + + // Then + assertFalse(result); + } + + @Test + @DisplayName("模式2:当deviceCount为0时应该返回false") + void shouldReturnFalseWhenDeviceCountIsZeroInMode2() throws Exception { + // Given + String conditionValueJson = "{\"deviceCount\": 0, \"deviceIds\": [200, 300]}"; + JsonNode conditionValue = objectMapper.readTree(conditionValueJson); + + List contextDeviceIds = Arrays.asList(200L, 300L); + ConditionContext context = ConditionContext.builder() + .deviceCount(2) + .deviceIds(contextDeviceIds) + .build(); + + // When + boolean result = strategy.evaluate(conditionValue, context); + + // Then + assertFalse(result); } } diff --git a/src/test/java/com/ycwl/basic/puzzle/fill/condition/DeviceCountRangeConditionStrategyTest.java b/src/test/java/com/ycwl/basic/puzzle/fill/condition/DeviceCountRangeConditionStrategyTest.java deleted file mode 100644 index b7cfd3e5..00000000 --- a/src/test/java/com/ycwl/basic/puzzle/fill/condition/DeviceCountRangeConditionStrategyTest.java +++ /dev/null @@ -1,190 +0,0 @@ -package com.ycwl.basic.puzzle.fill.condition; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * 机位数量范围匹配策略测试 - */ -@DisplayName("机位数量范围匹配策略测试") -class DeviceCountRangeConditionStrategyTest { - - private DeviceCountRangeConditionStrategy strategy; - private ObjectMapper objectMapper; - - @BeforeEach - void setUp() { - strategy = new DeviceCountRangeConditionStrategy(); - objectMapper = new ObjectMapper(); - } - - @Test - @DisplayName("应该返回正确的支持类型") - void shouldReturnCorrectSupportedType() { - assertEquals("DEVICE_COUNT_RANGE", strategy.getSupportedType()); - } - - @Test - @DisplayName("当机位数量在范围内时应该返回true") - void shouldReturnTrueWhenDeviceCountInRange() throws Exception { - // Given - String conditionValueJson = "{\"minCount\": 2, \"maxCount\": 5}"; - JsonNode conditionValue = objectMapper.readTree(conditionValueJson); - - ConditionContext context = ConditionContext.builder() - .deviceCount(3) - .build(); - - // When - boolean result = strategy.evaluate(conditionValue, context); - - // Then - assertTrue(result); - } - - @Test - @DisplayName("当机位数量等于最小值时应该返回true") - void shouldReturnTrueWhenDeviceCountEqualsMin() throws Exception { - // Given - String conditionValueJson = "{\"minCount\": 2, \"maxCount\": 5}"; - JsonNode conditionValue = objectMapper.readTree(conditionValueJson); - - ConditionContext context = ConditionContext.builder() - .deviceCount(2) - .build(); - - // When - boolean result = strategy.evaluate(conditionValue, context); - - // Then - assertTrue(result); - } - - @Test - @DisplayName("当机位数量等于最大值时应该返回true") - void shouldReturnTrueWhenDeviceCountEqualsMax() throws Exception { - // Given - String conditionValueJson = "{\"minCount\": 2, \"maxCount\": 5}"; - JsonNode conditionValue = objectMapper.readTree(conditionValueJson); - - ConditionContext context = ConditionContext.builder() - .deviceCount(5) - .build(); - - // When - boolean result = strategy.evaluate(conditionValue, context); - - // Then - assertTrue(result); - } - - @Test - @DisplayName("当机位数量小于最小值时应该返回false") - void shouldReturnFalseWhenDeviceCountBelowMin() throws Exception { - // Given - String conditionValueJson = "{\"minCount\": 2, \"maxCount\": 5}"; - JsonNode conditionValue = objectMapper.readTree(conditionValueJson); - - ConditionContext context = ConditionContext.builder() - .deviceCount(1) - .build(); - - // When - boolean result = strategy.evaluate(conditionValue, context); - - // Then - assertFalse(result); - } - - @Test - @DisplayName("当机位数量大于最大值时应该返回false") - void shouldReturnFalseWhenDeviceCountAboveMax() throws Exception { - // Given - String conditionValueJson = "{\"minCount\": 2, \"maxCount\": 5}"; - JsonNode conditionValue = objectMapper.readTree(conditionValueJson); - - ConditionContext context = ConditionContext.builder() - .deviceCount(6) - .build(); - - // When - boolean result = strategy.evaluate(conditionValue, context); - - // Then - assertFalse(result); - } - - @Test - @DisplayName("仅指定最小值时应该正确验证") - void shouldValidateWithMinCountOnly() throws Exception { - // Given - String conditionValueJson = "{\"minCount\": 2}"; - JsonNode conditionValue = objectMapper.readTree(conditionValueJson); - - ConditionContext context = ConditionContext.builder() - .deviceCount(3) - .build(); - - // When - boolean result = strategy.evaluate(conditionValue, context); - - // Then - assertTrue(result); - } - - @Test - @DisplayName("仅指定最大值时应该正确验证") - void shouldValidateWithMaxCountOnly() throws Exception { - // Given - String conditionValueJson = "{\"maxCount\": 5}"; - JsonNode conditionValue = objectMapper.readTree(conditionValueJson); - - ConditionContext context = ConditionContext.builder() - .deviceCount(3) - .build(); - - // When - boolean result = strategy.evaluate(conditionValue, context); - - // Then - assertTrue(result); - } - - @Test - @DisplayName("当conditionValue为null时应该返回false") - void shouldReturnFalseWhenConditionValueIsNull() { - // Given - ConditionContext context = ConditionContext.builder() - .deviceCount(3) - .build(); - - // When - boolean result = strategy.evaluate(null, context); - - // Then - assertFalse(result); - } - - @Test - @DisplayName("当context中deviceCount为null时应该返回false") - void shouldReturnFalseWhenContextDeviceCountIsNull() throws Exception { - // Given - String conditionValueJson = "{\"minCount\": 2, \"maxCount\": 5}"; - JsonNode conditionValue = objectMapper.readTree(conditionValueJson); - - ConditionContext context = ConditionContext.builder() - .deviceCount(null) - .build(); - - // When - boolean result = strategy.evaluate(conditionValue, context); - - // Then - assertFalse(result); - } -}