feat(puzzle): 增强拼图填充引擎功能

- 新增 requireRuleMatch 参数控制是否必须匹配规则
- 重构 DeviceCountConditionStrategy 支持两种匹配模式
- 移除已废弃的 DeviceCountRangeConditionStrategy
- 引入 FillResult 类封装填充结果信息
- 优化条件上下文和数据源上下文的 extra 字段类型
- 更新相关测试用例和文档说明
This commit is contained in:
2025-11-20 15:11:13 +08:00
parent aaa8d8310a
commit 2fd852c5c6
13 changed files with 470 additions and 261 deletions

View File

@@ -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"}` |
**数据源类型**

View File

@@ -56,4 +56,11 @@ public class PuzzleGenerateRequest {
* 仅对JPEG格式有效
*/
private Integer quality;
/**
* 是否必须匹配填充规则才能生成(可选,默认false)
* true: 如果没有规则匹配,抛出异常,不生成图片
* false: 无论是否匹配规则,都继续生成(默认行为)
*/
private Boolean requireRuleMatch = false;
}

View 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();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,11 +13,6 @@ public enum ConditionType {
*/
DEVICE_COUNT("DEVICE_COUNT", "机位数量匹配"),
/**
* 机位数量范围
*/
DEVICE_COUNT_RANGE("DEVICE_COUNT_RANGE", "机位数量范围"),
/**
* 机位ID精确匹配
*/

View File

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

View File

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

View File

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