You've already forked FrameTour-BE
feat(puzzle): 增强拼图填充引擎功能
- 新增 requireRuleMatch 参数控制是否必须匹配规则 - 重构 DeviceCountConditionStrategy 支持两种匹配模式 - 移除已废弃的 DeviceCountRangeConditionStrategy - 引入 FillResult 类封装填充结果信息 - 优化条件上下文和数据源上下文的 extra 字段类型 - 更新相关测试用例和文档说明
This commit is contained in:
@@ -259,8 +259,8 @@ Map<String, String> 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"}` |
|
||||
|
||||
**数据源类型**:
|
||||
|
||||
@@ -56,4 +56,11 @@ public class PuzzleGenerateRequest {
|
||||
* 仅对JPEG格式有效
|
||||
*/
|
||||
private Integer quality;
|
||||
|
||||
/**
|
||||
* 是否必须匹配填充规则才能生成(可选,默认false)
|
||||
* true: 如果没有规则匹配,抛出异常,不生成图片
|
||||
* false: 无论是否匹配规则,都继续生成(默认行为)
|
||||
*/
|
||||
private Boolean requireRuleMatch = false;
|
||||
}
|
||||
|
||||
66
src/main/java/com/ycwl/basic/puzzle/fill/FillResult.java
Normal file
66
src/main/java/com/ycwl/basic/puzzle/fill/FillResult.java
Normal file
@@ -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<String, String> 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<String, String> data, int count) {
|
||||
return FillResult.builder()
|
||||
.ruleMatched(true)
|
||||
.matchedRuleName(ruleName)
|
||||
.dynamicData(data)
|
||||
.filledCount(count)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -50,14 +50,12 @@ public class PuzzleElementFillEngine {
|
||||
* @param templateId 模板ID
|
||||
* @param faceId 人脸ID
|
||||
* @param scenicId 景区ID
|
||||
* @return 填充后的dynamicData
|
||||
* @return 填充结果(包含是否匹配规则的信息)
|
||||
*/
|
||||
public Map<String, String> execute(Long templateId, Long faceId, Long scenicId) {
|
||||
Map<String, String> 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<PuzzleFillRuleEntity> 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<String, String> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, Object> extra;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 机位数量精确匹配策略
|
||||
*
|
||||
* <p>支持两种匹配模式:</p>
|
||||
* <ul>
|
||||
* <li>模式1:全局数量匹配 - 只指定 deviceCount,匹配所有机位的数量</li>
|
||||
* <li>模式2:指定列表数量匹配 - 同时指定 deviceCount + deviceIds,从指定列表中过滤并匹配数量</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>配置示例:</p>
|
||||
* <pre>
|
||||
* // 模式1:全局数量匹配
|
||||
* {
|
||||
* "deviceCount": 4
|
||||
* }
|
||||
*
|
||||
* // 模式2:指定列表数量匹配
|
||||
* {
|
||||
* "deviceCount": 2,
|
||||
* "deviceIds": [200, 300, 400]
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>模式2匹配逻辑:</p>
|
||||
* <ul>
|
||||
* <li>从 deviceIds 列表中过滤出实际存在的机位</li>
|
||||
* <li>保持配置顺序(不按 deviceId 排序)</li>
|
||||
* <li>判断过滤后的数量是否等于 deviceCount</li>
|
||||
* <li>匹配成功后,将过滤后的机位列表存入 context.extra,供数据源解析使用</li>
|
||||
* </ul>
|
||||
*
|
||||
* @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<Long> requiredDeviceIds = new ArrayList<>();
|
||||
deviceIdsNode.forEach(node -> requiredDeviceIds.add(node.asLong()));
|
||||
|
||||
if (requiredDeviceIds.isEmpty()) {
|
||||
log.warn("deviceIds数组为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 获取上下文中的机位列表
|
||||
List<Long> contextDeviceIds = context.getDeviceIds();
|
||||
if (contextDeviceIds == null || contextDeviceIds.isEmpty()) {
|
||||
log.debug("上下文中没有机位ID列表");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. 按配置顺序过滤出实际存在的机位
|
||||
List<Long> matchedDeviceIds = requiredDeviceIds.stream()
|
||||
.filter(contextDeviceIds::contains)
|
||||
.collect(Collectors.toList()); // 保持配置顺序,不排序
|
||||
|
||||
// 4. 精确匹配数量
|
||||
boolean matched = matchedDeviceIds.size() == expectedCount;
|
||||
|
||||
if (matched) {
|
||||
// 5. 将过滤后的机位列表存入 context.extra
|
||||
Map<String, Object> 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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> extra;
|
||||
}
|
||||
|
||||
@@ -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<String, Object> extra = context.getExtra();
|
||||
if (extra != null && extra.containsKey("filteredDeviceIds")) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Long> filteredDeviceIds = (List<Long>) 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;
|
||||
}
|
||||
|
||||
@@ -13,11 +13,6 @@ public enum ConditionType {
|
||||
*/
|
||||
DEVICE_COUNT("DEVICE_COUNT", "机位数量匹配"),
|
||||
|
||||
/**
|
||||
* 机位数量范围
|
||||
*/
|
||||
DEVICE_COUNT_RANGE("DEVICE_COUNT_RANGE", "机位数量范围"),
|
||||
|
||||
/**
|
||||
* 机位ID精确匹配
|
||||
*/
|
||||
|
||||
@@ -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<String, String> 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());
|
||||
|
||||
@@ -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<Long> 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<Long> filteredIds = (List<Long>) 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<Long> 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<Long> 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<Long> 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<Long> filteredIds = (List<Long>) 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<Long> 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<Long> 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<Long> contextDeviceIds = Arrays.asList(200L, 300L);
|
||||
ConditionContext context = ConditionContext.builder()
|
||||
.deviceCount(2)
|
||||
.deviceIds(contextDeviceIds)
|
||||
.build();
|
||||
|
||||
// When
|
||||
boolean result = strategy.evaluate(conditionValue, context);
|
||||
|
||||
// Then
|
||||
assertFalse(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user