feat(puzzle): 实现拼图自动填充规则引擎及相关功能

- 新增拼图填充规则管理Controller、DTO、Entity等核心类
- 实现条件评估策略模式,支持多种匹配规则
- 实现数据源解析策略模式,支持多种数据来源
- 新增拼图元素自动填充引擎,支持优先级匹配和动态填充
- 在SourceMapper中增加设备统计和查询相关方法
- 在PuzzleGenerateRequest中新增faceId字段用于触发自动填充
- 完善相关枚举类和工具类,提升系统可维护性和扩展性
This commit is contained in:
2025-11-19 11:10:23 +08:00
parent de421cf0d5
commit 778afaaa83
43 changed files with 4019 additions and 3 deletions

View File

@@ -0,0 +1,400 @@
package com.ycwl.basic.puzzle.fill;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
import com.ycwl.basic.puzzle.entity.PuzzleFillRuleEntity;
import com.ycwl.basic.puzzle.entity.PuzzleFillRuleItemEntity;
import com.ycwl.basic.puzzle.fill.condition.ConditionEvaluator;
import com.ycwl.basic.puzzle.fill.datasource.DataSourceResolver;
import com.ycwl.basic.puzzle.mapper.PuzzleFillRuleItemMapper;
import com.ycwl.basic.puzzle.mapper.PuzzleFillRuleMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* 拼图元素填充引擎测试
*/
@DisplayName("拼图元素填充引擎测试")
@ExtendWith(MockitoExtension.class)
class PuzzleElementFillEngineTest {
@Mock
private PuzzleFillRuleMapper ruleMapper;
@Mock
private PuzzleFillRuleItemMapper itemMapper;
@Mock
private SourceMapper sourceMapper;
@Mock
private ConditionEvaluator conditionEvaluator;
@Mock
private DataSourceResolver dataSourceResolver;
@InjectMocks
private PuzzleElementFillEngine engine;
@BeforeEach
void setUp() {
// 默认设置
}
@Test
@DisplayName("当没有配置规则时应该返回空Map")
void shouldReturnEmptyMapWhenNoRulesConfigured() {
// Given
Long templateId = 1L;
Long faceId = 123L;
Long scenicId = 1L;
when(ruleMapper.listByTemplateAndScenic(templateId, scenicId))
.thenReturn(new ArrayList<>());
// When
Map<String, String> result = engine.execute(templateId, faceId, scenicId);
// Then
assertTrue(result.isEmpty());
verify(ruleMapper, times(1)).listByTemplateAndScenic(templateId, scenicId);
verify(sourceMapper, never()).countDistinctDevicesByFaceId(anyLong());
}
@Test
@DisplayName("应该成功执行匹配的规则并填充多个元素")
void shouldExecuteMatchedRuleAndFillMultipleElements() {
// Given
Long templateId = 1L;
Long faceId = 123L;
Long scenicId = 1L;
// 模拟规则
PuzzleFillRuleEntity rule = createRule(1L, "4机位规则", 100);
when(ruleMapper.listByTemplateAndScenic(templateId, scenicId))
.thenReturn(Arrays.asList(rule));
// 模拟机位数量和机位列表
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
// 模拟条件匹配
when(conditionEvaluator.evaluate(eq(rule), any())).thenReturn(true);
// 模拟规则明细
List<PuzzleFillRuleItemEntity> items = Arrays.asList(
createItem(1L, 1L, "sfp_1", "DEVICE_IMAGE", "{\"deviceIndex\":0}", "LATEST"),
createItem(2L, 1L, "sfp_2", "DEVICE_IMAGE", "{\"deviceIndex\":1}", "LATEST"),
createItem(3L, 1L, "sfp_3", "DEVICE_IMAGE", "{\"deviceIndex\":2}", "LATEST"),
createItem(4L, 1L, "sfp_4", "DEVICE_IMAGE", "{\"deviceIndex\":3}", "LATEST")
);
when(itemMapper.listByRuleId(1L)).thenReturn(items);
// 模拟数据源解析
when(dataSourceResolver.resolve(anyString(), anyString(), anyString(), anyString(), any()))
.thenReturn("https://oss.example.com/img1.jpg")
.thenReturn("https://oss.example.com/img2.jpg")
.thenReturn("https://oss.example.com/img3.jpg")
.thenReturn("https://oss.example.com/img4.jpg");
// When
Map<String, String> result = engine.execute(templateId, faceId, scenicId);
// Then
assertEquals(4, result.size());
assertEquals("https://oss.example.com/img1.jpg", result.get("sfp_1"));
assertEquals("https://oss.example.com/img2.jpg", result.get("sfp_2"));
assertEquals("https://oss.example.com/img3.jpg", result.get("sfp_3"));
assertEquals("https://oss.example.com/img4.jpg", result.get("sfp_4"));
verify(conditionEvaluator, times(1)).evaluate(eq(rule), any());
verify(itemMapper, times(1)).listByRuleId(1L);
verify(dataSourceResolver, times(4)).resolve(anyString(), anyString(), anyString(), anyString(), any());
}
@Test
@DisplayName("应该按优先级顺序评估规则并在匹配第一条后停止")
void shouldEvaluateRulesByPriorityAndStopAfterFirstMatch() {
// Given
Long templateId = 1L;
Long faceId = 123L;
Long scenicId = 1L;
// 模拟3条规则(按priority DESC排序)
PuzzleFillRuleEntity rule1 = createRule(1L, "高优先级规则", 100);
PuzzleFillRuleEntity rule2 = createRule(2L, "中优先级规则", 50);
PuzzleFillRuleEntity rule3 = createRule(3L, "低优先级规则", 10);
when(ruleMapper.listByTemplateAndScenic(templateId, scenicId))
.thenReturn(Arrays.asList(rule1, rule2, rule3));
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
// 模拟第一条规则不匹配,第二条匹配
when(conditionEvaluator.evaluate(eq(rule1), any())).thenReturn(false);
when(conditionEvaluator.evaluate(eq(rule2), any())).thenReturn(true);
// 模拟规则2的明细
List<PuzzleFillRuleItemEntity> items = Arrays.asList(
createItem(1L, 2L, "sfp_1", "DEVICE_IMAGE", "{}", "LATEST")
);
when(itemMapper.listByRuleId(2L)).thenReturn(items);
when(dataSourceResolver.resolve(anyString(), anyString(), anyString(), anyString(), any()))
.thenReturn("https://oss.example.com/img.jpg");
// When
Map<String, String> result = engine.execute(templateId, faceId, scenicId);
// Then
assertEquals(1, result.size());
assertEquals("https://oss.example.com/img.jpg", result.get("sfp_1"));
// 应该评估了rule1和rule2,但没有评估rule3(因为rule2匹配后停止)
verify(conditionEvaluator, times(1)).evaluate(eq(rule1), any());
verify(conditionEvaluator, times(1)).evaluate(eq(rule2), any());
verify(conditionEvaluator, never()).evaluate(eq(rule3), any());
verify(itemMapper, times(1)).listByRuleId(2L);
verify(itemMapper, never()).listByRuleId(1L);
verify(itemMapper, never()).listByRuleId(3L);
}
@Test
@DisplayName("当所有规则都不匹配时应该返回空Map")
void shouldReturnEmptyMapWhenNoRuleMatches() {
// Given
Long templateId = 1L;
Long faceId = 123L;
Long scenicId = 1L;
PuzzleFillRuleEntity rule1 = createRule(1L, "规则1", 100);
PuzzleFillRuleEntity rule2 = createRule(2L, "规则2", 50);
when(ruleMapper.listByTemplateAndScenic(templateId, scenicId))
.thenReturn(Arrays.asList(rule1, rule2));
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
// 所有规则都不匹配
when(conditionEvaluator.evaluate(any(), any())).thenReturn(false);
// When
Map<String, String> result = engine.execute(templateId, faceId, scenicId);
// Then
assertTrue(result.isEmpty());
verify(conditionEvaluator, times(2)).evaluate(any(), any());
verify(itemMapper, never()).listByRuleId(anyLong());
}
@Test
@DisplayName("当数据源解析返回null时应该跳过该元素")
void shouldSkipElementWhenDataSourceReturnsNull() {
// Given
Long templateId = 1L;
Long faceId = 123L;
Long scenicId = 1L;
PuzzleFillRuleEntity rule = createRule(1L, "测试规则", 100);
when(ruleMapper.listByTemplateAndScenic(templateId, scenicId))
.thenReturn(Arrays.asList(rule));
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
when(conditionEvaluator.evaluate(eq(rule), any())).thenReturn(true);
List<PuzzleFillRuleItemEntity> items = Arrays.asList(
createItem(1L, 1L, "sfp_1", "DEVICE_IMAGE", "{}", "LATEST"),
createItem(2L, 1L, "sfp_2", "DEVICE_IMAGE", "{}", "LATEST")
);
when(itemMapper.listByRuleId(1L)).thenReturn(items);
// 第一个返回值,第二个返回null
when(dataSourceResolver.resolve(anyString(), anyString(), anyString(), anyString(), any()))
.thenReturn("https://oss.example.com/img1.jpg")
.thenReturn(null);
// When
Map<String, String> result = engine.execute(templateId, faceId, scenicId);
// Then
assertEquals(1, result.size());
assertEquals("https://oss.example.com/img1.jpg", result.get("sfp_1"));
assertFalse(result.containsKey("sfp_2"));
}
@Test
@DisplayName("当数据源解析失败时应该使用fallbackValue")
void shouldUseFallbackValueWhenDataSourceFails() {
// Given
Long templateId = 1L;
Long faceId = 123L;
Long scenicId = 1L;
PuzzleFillRuleEntity rule = createRule(1L, "测试规则", 100);
when(ruleMapper.listByTemplateAndScenic(templateId, scenicId))
.thenReturn(Arrays.asList(rule));
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
when(conditionEvaluator.evaluate(eq(rule), any())).thenReturn(true);
// 明细包含fallbackValue
PuzzleFillRuleItemEntity item = createItem(1L, 1L, "sfp_1", "DEVICE_IMAGE", "{}", "LATEST");
item.setFallbackValue("https://oss.example.com/default.jpg");
when(itemMapper.listByRuleId(1L)).thenReturn(Arrays.asList(item));
// DataSourceResolver内部会处理fallback,这里模拟返回fallback值
when(dataSourceResolver.resolve(anyString(), anyString(), anyString(), eq("https://oss.example.com/default.jpg"), any()))
.thenReturn("https://oss.example.com/default.jpg");
// When
Map<String, String> result = engine.execute(templateId, faceId, scenicId);
// Then
assertEquals(1, result.size());
assertEquals("https://oss.example.com/default.jpg", result.get("sfp_1"));
}
@Test
@DisplayName("当规则匹配但没有明细时应该返回空Map")
void shouldReturnEmptyMapWhenRuleMatchesButHasNoItems() {
// Given
Long templateId = 1L;
Long faceId = 123L;
Long scenicId = 1L;
PuzzleFillRuleEntity rule = createRule(1L, "空明细规则", 100);
when(ruleMapper.listByTemplateAndScenic(templateId, scenicId))
.thenReturn(Arrays.asList(rule));
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
when(conditionEvaluator.evaluate(eq(rule), any())).thenReturn(true);
// 规则没有明细
when(itemMapper.listByRuleId(1L)).thenReturn(new ArrayList<>());
// When
Map<String, String> result = engine.execute(templateId, faceId, scenicId);
// Then
assertTrue(result.isEmpty());
}
@Test
@DisplayName("当发生异常时应该返回空Map并记录日志")
void shouldReturnEmptyMapAndLogWhenExceptionOccurs() {
// Given
Long templateId = 1L;
Long faceId = 123L;
Long scenicId = 1L;
when(ruleMapper.listByTemplateAndScenic(templateId, scenicId))
.thenThrow(new RuntimeException("Database error"));
// When
Map<String, String> result = engine.execute(templateId, faceId, scenicId);
// Then
assertTrue(result.isEmpty());
}
@Test
@DisplayName("应该支持DEVICE_ID_MATCH条件类型并正确填充")
void shouldSupportDeviceIdMatchCondition() {
// Given
Long templateId = 1L;
Long faceId = 123L;
Long scenicId = 1L;
// 模拟规则 - 使用DEVICE_ID_MATCH条件
PuzzleFillRuleEntity rule = new PuzzleFillRuleEntity();
rule.setId(1L);
rule.setRuleName("指定机位规则");
rule.setPriority(100);
rule.setConditionType("DEVICE_ID_MATCH");
rule.setConditionValue("{\"deviceIds\": [200, 300], \"matchMode\": \"ALL\"}");
rule.setEnabled(1);
when(ruleMapper.listByTemplateAndScenic(templateId, scenicId))
.thenReturn(Arrays.asList(rule));
// 模拟机位数量和机位列表
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
// 模拟条件匹配
when(conditionEvaluator.evaluate(eq(rule), any())).thenReturn(true);
// 模拟规则明细
List<PuzzleFillRuleItemEntity> items = Arrays.asList(
createItem(1L, 1L, "sfp_1", "DEVICE_IMAGE", "{\"deviceIndex\":1}", "LATEST"),
createItem(2L, 1L, "sfp_2", "DEVICE_IMAGE", "{\"deviceIndex\":2}", "LATEST")
);
when(itemMapper.listByRuleId(1L)).thenReturn(items);
// 模拟数据源解析
when(dataSourceResolver.resolve(anyString(), anyString(), anyString(), anyString(), any()))
.thenReturn("https://oss.example.com/device200.jpg")
.thenReturn("https://oss.example.com/device300.jpg");
// When
Map<String, String> result = engine.execute(templateId, faceId, scenicId);
// Then
assertEquals(2, result.size());
assertEquals("https://oss.example.com/device200.jpg", result.get("sfp_1"));
assertEquals("https://oss.example.com/device300.jpg", result.get("sfp_2"));
// 验证deviceIds被传递到ConditionContext
verify(conditionEvaluator, times(1)).evaluate(eq(rule), any());
verify(sourceMapper, times(1)).getDeviceIdsByFaceId(faceId);
}
// 辅助方法
private PuzzleFillRuleEntity createRule(Long id, String name, Integer priority) {
PuzzleFillRuleEntity rule = new PuzzleFillRuleEntity();
rule.setId(id);
rule.setRuleName(name);
rule.setPriority(priority);
rule.setConditionType("DEVICE_COUNT");
rule.setConditionValue("{\"deviceCount\": 4}");
rule.setEnabled(1);
return rule;
}
private PuzzleFillRuleItemEntity createItem(Long id, Long ruleId, String elementKey,
String dataSource, String sourceFilter, String sortStrategy) {
PuzzleFillRuleItemEntity item = new PuzzleFillRuleItemEntity();
item.setId(id);
item.setRuleId(ruleId);
item.setElementKey(elementKey);
item.setDataSource(dataSource);
item.setSourceFilter(sourceFilter);
item.setSortStrategy(sortStrategy);
item.setItemOrder(id.intValue());
return item;
}
}

View File

@@ -0,0 +1,80 @@
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 AlwaysConditionStrategyTest {
private AlwaysConditionStrategy strategy;
private ObjectMapper objectMapper;
@BeforeEach
void setUp() {
strategy = new AlwaysConditionStrategy();
objectMapper = new ObjectMapper();
}
@Test
@DisplayName("应该返回正确的支持类型")
void shouldReturnCorrectSupportedType() {
assertEquals("ALWAYS", strategy.getSupportedType());
}
@Test
@DisplayName("无论conditionValue和context如何都应该返回true")
void shouldAlwaysReturnTrue() throws Exception {
// Given
String conditionValueJson = "{\"anyField\": \"anyValue\"}";
JsonNode conditionValue = objectMapper.readTree(conditionValueJson);
ConditionContext context = ConditionContext.builder()
.deviceCount(4)
.faceId(123L)
.scenicId(1L)
.build();
// When
boolean result = strategy.evaluate(conditionValue, context);
// Then
assertTrue(result);
}
@Test
@DisplayName("当conditionValue为null时也应该返回true")
void shouldReturnTrueEvenWhenConditionValueIsNull() {
// Given
ConditionContext context = ConditionContext.builder().build();
// When
boolean result = strategy.evaluate(null, context);
// Then
assertTrue(result);
}
@Test
@DisplayName("当context为空时也应该返回true")
void shouldReturnTrueEvenWhenContextIsEmpty() throws Exception {
// Given
String conditionValueJson = "{}";
JsonNode conditionValue = objectMapper.readTree(conditionValueJson);
ConditionContext context = ConditionContext.builder().build();
// When
boolean result = strategy.evaluate(conditionValue, context);
// Then
assertTrue(result);
}
}

View File

@@ -0,0 +1,239 @@
package com.ycwl.basic.puzzle.fill.condition;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ycwl.basic.puzzle.entity.PuzzleFillRuleEntity;
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.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* 条件评估器集成测试
*/
@DisplayName("条件评估器集成测试")
class ConditionEvaluatorTest {
private ConditionEvaluator evaluator;
private ObjectMapper objectMapper;
@BeforeEach
void setUp() {
objectMapper = new ObjectMapper();
// 注册所有策略
List<ConditionStrategy> strategies = Arrays.asList(
new DeviceCountConditionStrategy(),
new DeviceCountRangeConditionStrategy(),
new DeviceIdMatchConditionStrategy(),
new AlwaysConditionStrategy()
);
evaluator = new ConditionEvaluator(strategies, objectMapper);
}
@Test
@DisplayName("应该正确评估DEVICE_COUNT类型的规则")
void shouldEvaluateDeviceCountRule() {
// Given
PuzzleFillRuleEntity rule = new PuzzleFillRuleEntity();
rule.setId(1L);
rule.setRuleName("4机位规则");
rule.setConditionType("DEVICE_COUNT");
rule.setConditionValue("{\"deviceCount\": 4}");
ConditionContext context = ConditionContext.builder()
.deviceCount(4)
.build();
// When
boolean result = evaluator.evaluate(rule, context);
// Then
assertTrue(result);
}
@Test
@DisplayName("应该正确评估DEVICE_COUNT_RANGE类型的规则")
void shouldEvaluateDeviceCountRangeRule() {
// Given
PuzzleFillRuleEntity rule = new PuzzleFillRuleEntity();
rule.setId(2L);
rule.setRuleName("2-5机位规则");
rule.setConditionType("DEVICE_COUNT_RANGE");
rule.setConditionValue("{\"minCount\": 2, \"maxCount\": 5}");
ConditionContext context = ConditionContext.builder()
.deviceCount(3)
.build();
// When
boolean result = evaluator.evaluate(rule, context);
// Then
assertTrue(result);
}
@Test
@DisplayName("应该正确评估ALWAYS类型的规则")
void shouldEvaluateAlwaysRule() {
// Given
PuzzleFillRuleEntity rule = new PuzzleFillRuleEntity();
rule.setId(3L);
rule.setRuleName("兜底规则");
rule.setConditionType("ALWAYS");
rule.setConditionValue("{}");
ConditionContext context = ConditionContext.builder()
.deviceCount(999)
.build();
// When
boolean result = evaluator.evaluate(rule, context);
// Then
assertTrue(result);
}
@Test
@DisplayName("应该正确评估DEVICE_ID_MATCH类型的规则-单机位匹配")
void shouldEvaluateDeviceIdMatchRuleSingleDevice() {
// Given
PuzzleFillRuleEntity rule = new PuzzleFillRuleEntity();
rule.setId(4L);
rule.setRuleName("指定机位规则");
rule.setConditionType("DEVICE_ID_MATCH");
rule.setConditionValue("{\"deviceId\": 200}");
ConditionContext context = ConditionContext.builder()
.deviceIds(Arrays.asList(100L, 200L, 300L))
.build();
// When
boolean result = evaluator.evaluate(rule, context);
// Then
assertTrue(result);
}
@Test
@DisplayName("应该正确评估DEVICE_ID_MATCH类型的规则-多机位ANY匹配")
void shouldEvaluateDeviceIdMatchRuleMultipleDevicesAny() {
// Given
PuzzleFillRuleEntity rule = new PuzzleFillRuleEntity();
rule.setId(5L);
rule.setRuleName("多机位ANY规则");
rule.setConditionType("DEVICE_ID_MATCH");
rule.setConditionValue("{\"deviceIds\": [200, 400], \"matchMode\": \"ANY\"}");
ConditionContext context = ConditionContext.builder()
.deviceIds(Arrays.asList(100L, 200L, 300L))
.build();
// When
boolean result = evaluator.evaluate(rule, context);
// Then
assertTrue(result);
}
@Test
@DisplayName("应该正确评估DEVICE_ID_MATCH类型的规则-多机位ALL匹配")
void shouldEvaluateDeviceIdMatchRuleMultipleDevicesAll() {
// Given
PuzzleFillRuleEntity rule = new PuzzleFillRuleEntity();
rule.setId(6L);
rule.setRuleName("多机位ALL规则");
rule.setConditionType("DEVICE_ID_MATCH");
rule.setConditionValue("{\"deviceIds\": [100, 200], \"matchMode\": \"ALL\"}");
ConditionContext context = ConditionContext.builder()
.deviceIds(Arrays.asList(100L, 200L, 300L))
.build();
// When
boolean result = evaluator.evaluate(rule, context);
// Then
assertTrue(result);
}
@Test
@DisplayName("当条件类型不存在时应该返回false")
void shouldReturnFalseWhenConditionTypeNotFound() {
// Given
PuzzleFillRuleEntity rule = new PuzzleFillRuleEntity();
rule.setId(4L);
rule.setRuleName("未知类型规则");
rule.setConditionType("UNKNOWN_TYPE");
rule.setConditionValue("{}");
ConditionContext context = ConditionContext.builder().build();
// When
boolean result = evaluator.evaluate(rule, context);
// Then
assertFalse(result);
}
@Test
@DisplayName("当conditionValue格式错误时应该返回false")
void shouldReturnFalseWhenConditionValueIsInvalid() {
// Given
PuzzleFillRuleEntity rule = new PuzzleFillRuleEntity();
rule.setId(5L);
rule.setRuleName("格式错误规则");
rule.setConditionType("DEVICE_COUNT");
rule.setConditionValue("invalid json");
ConditionContext context = ConditionContext.builder()
.deviceCount(4)
.build();
// When
boolean result = evaluator.evaluate(rule, context);
// Then
assertFalse(result);
}
@Test
@DisplayName("应该支持多个策略并正确路由")
void shouldSupportMultipleStrategies() {
// Test DEVICE_COUNT
ConditionContext context1 = ConditionContext.builder()
.deviceCount(4)
.build();
PuzzleFillRuleEntity rule1 = new PuzzleFillRuleEntity();
rule1.setConditionType("DEVICE_COUNT");
rule1.setConditionValue("{\"deviceCount\": 4}");
assertTrue(evaluator.evaluate(rule1, context1));
// Test DEVICE_COUNT_RANGE
PuzzleFillRuleEntity rule2 = new PuzzleFillRuleEntity();
rule2.setConditionType("DEVICE_COUNT_RANGE");
rule2.setConditionValue("{\"minCount\": 2, \"maxCount\": 5}");
assertTrue(evaluator.evaluate(rule2, context1));
// Test DEVICE_ID_MATCH
ConditionContext context2 = ConditionContext.builder()
.deviceIds(Arrays.asList(100L, 200L, 300L))
.build();
PuzzleFillRuleEntity rule3 = new PuzzleFillRuleEntity();
rule3.setConditionType("DEVICE_ID_MATCH");
rule3.setConditionValue("{\"deviceId\": 200}");
assertTrue(evaluator.evaluate(rule3, context2));
// Test ALWAYS
PuzzleFillRuleEntity rule4 = new PuzzleFillRuleEntity();
rule4.setConditionType("ALWAYS");
rule4.setConditionValue("{}");
assertTrue(evaluator.evaluate(rule4, context1));
}
}

View File

@@ -0,0 +1,136 @@
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 DeviceCountConditionStrategyTest {
private DeviceCountConditionStrategy strategy;
private ObjectMapper objectMapper;
@BeforeEach
void setUp() {
strategy = new DeviceCountConditionStrategy();
objectMapper = new ObjectMapper();
}
@Test
@DisplayName("应该返回正确的支持类型")
void shouldReturnCorrectSupportedType() {
assertEquals("DEVICE_COUNT", strategy.getSupportedType());
}
@Test
@DisplayName("当机位数量完全匹配时应该返回true")
void shouldReturnTrueWhenDeviceCountMatches() throws Exception {
// Given
String conditionValueJson = "{\"deviceCount\": 4}";
JsonNode conditionValue = objectMapper.readTree(conditionValueJson);
ConditionContext context = ConditionContext.builder()
.deviceCount(4)
.build();
// When
boolean result = strategy.evaluate(conditionValue, context);
// Then
assertTrue(result);
}
@Test
@DisplayName("当机位数量不匹配时应该返回false")
void shouldReturnFalseWhenDeviceCountDoesNotMatch() throws Exception {
// Given
String conditionValueJson = "{\"deviceCount\": 4}";
JsonNode conditionValue = objectMapper.readTree(conditionValueJson);
ConditionContext context = ConditionContext.builder()
.deviceCount(2)
.build();
// When
boolean result = strategy.evaluate(conditionValue, context);
// Then
assertFalse(result);
}
@Test
@DisplayName("当conditionValue为null时应该返回false")
void shouldReturnFalseWhenConditionValueIsNull() {
// Given
ConditionContext context = ConditionContext.builder()
.deviceCount(4)
.build();
// When
boolean result = strategy.evaluate(null, context);
// Then
assertFalse(result);
}
@Test
@DisplayName("当conditionValue缺少deviceCount字段时应该返回false")
void shouldReturnFalseWhenDeviceCountFieldIsMissing() throws Exception {
// Given
String conditionValueJson = "{\"otherField\": 4}";
JsonNode conditionValue = objectMapper.readTree(conditionValueJson);
ConditionContext context = ConditionContext.builder()
.deviceCount(4)
.build();
// When
boolean result = strategy.evaluate(conditionValue, context);
// Then
assertFalse(result);
}
@Test
@DisplayName("当context中deviceCount为null时应该返回false")
void shouldReturnFalseWhenContextDeviceCountIsNull() throws Exception {
// Given
String conditionValueJson = "{\"deviceCount\": 4}";
JsonNode conditionValue = objectMapper.readTree(conditionValueJson);
ConditionContext context = ConditionContext.builder()
.deviceCount(null)
.build();
// When
boolean result = strategy.evaluate(conditionValue, context);
// Then
assertFalse(result);
}
@Test
@DisplayName("应该支持deviceCount为0的情况")
void shouldSupportZeroDeviceCount() throws Exception {
// Given
String conditionValueJson = "{\"deviceCount\": 0}";
JsonNode conditionValue = objectMapper.readTree(conditionValueJson);
ConditionContext context = ConditionContext.builder()
.deviceCount(0)
.build();
// When
boolean result = strategy.evaluate(conditionValue, context);
// Then
assertTrue(result);
}
}

View File

@@ -0,0 +1,190 @@
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);
}
}

View File

@@ -0,0 +1,341 @@
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 java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* 机位ID匹配条件策略测试
*/
@DisplayName("机位ID匹配条件策略测试")
class DeviceIdMatchConditionStrategyTest {
private DeviceIdMatchConditionStrategy strategy;
private ObjectMapper objectMapper;
@BeforeEach
void setUp() {
strategy = new DeviceIdMatchConditionStrategy();
objectMapper = new ObjectMapper();
}
@Test
@DisplayName("应该返回正确的支持类型")
void shouldReturnCorrectSupportedType() {
assertEquals("DEVICE_ID_MATCH", strategy.getSupportedType());
}
@Test
@DisplayName("单机位匹配-机位存在应该返回true")
void shouldReturnTrueWhenSingleDeviceIdExists() throws Exception {
// Given
List<Long> deviceIds = Arrays.asList(100L, 200L, 300L);
ConditionContext context = ConditionContext.builder()
.deviceIds(deviceIds)
.build();
JsonNode conditionValue = objectMapper.readTree("{\"deviceId\": 200}");
// When
boolean result = strategy.evaluate(conditionValue, context);
// Then
assertTrue(result);
}
@Test
@DisplayName("单机位匹配-机位不存在应该返回false")
void shouldReturnFalseWhenSingleDeviceIdNotExists() throws Exception {
// Given
List<Long> deviceIds = Arrays.asList(100L, 200L, 300L);
ConditionContext context = ConditionContext.builder()
.deviceIds(deviceIds)
.build();
JsonNode conditionValue = objectMapper.readTree("{\"deviceId\": 999}");
// When
boolean result = strategy.evaluate(conditionValue, context);
// Then
assertFalse(result);
}
@Test
@DisplayName("多机位ANY匹配-至少存在一个机位应该返回true")
void shouldReturnTrueWhenAnyDeviceIdMatches() throws Exception {
// Given
List<Long> deviceIds = Arrays.asList(100L, 200L, 300L);
ConditionContext context = ConditionContext.builder()
.deviceIds(deviceIds)
.build();
// 200存在,400不存在,但ANY模式只需一个匹配
JsonNode conditionValue = objectMapper.readTree("{\"deviceIds\": [200, 400], \"matchMode\": \"ANY\"}");
// When
boolean result = strategy.evaluate(conditionValue, context);
// Then
assertTrue(result);
}
@Test
@DisplayName("多机位ANY匹配-没有任何机位存在应该返回false")
void shouldReturnFalseWhenNoDeviceIdMatches() throws Exception {
// Given
List<Long> deviceIds = Arrays.asList(100L, 200L, 300L);
ConditionContext context = ConditionContext.builder()
.deviceIds(deviceIds)
.build();
JsonNode conditionValue = objectMapper.readTree("{\"deviceIds\": [400, 500], \"matchMode\": \"ANY\"}");
// When
boolean result = strategy.evaluate(conditionValue, context);
// Then
assertFalse(result);
}
@Test
@DisplayName("多机位ALL匹配-所有机位都存在应该返回true")
void shouldReturnTrueWhenAllDeviceIdsMatch() throws Exception {
// Given
List<Long> deviceIds = Arrays.asList(100L, 200L, 300L);
ConditionContext context = ConditionContext.builder()
.deviceIds(deviceIds)
.build();
JsonNode conditionValue = objectMapper.readTree("{\"deviceIds\": [100, 200], \"matchMode\": \"ALL\"}");
// When
boolean result = strategy.evaluate(conditionValue, context);
// Then
assertTrue(result);
}
@Test
@DisplayName("多机位ALL匹配-部分机位不存在应该返回false")
void shouldReturnFalseWhenNotAllDeviceIdsMatch() throws Exception {
// Given
List<Long> deviceIds = Arrays.asList(100L, 200L, 300L);
ConditionContext context = ConditionContext.builder()
.deviceIds(deviceIds)
.build();
// 200存在但400不存在
JsonNode conditionValue = objectMapper.readTree("{\"deviceIds\": [200, 400], \"matchMode\": \"ALL\"}");
// When
boolean result = strategy.evaluate(conditionValue, context);
// Then
assertFalse(result);
}
@Test
@DisplayName("多机位匹配-未指定matchMode应该默认使用ANY模式")
void shouldDefaultToAnyModeWhenMatchModeNotSpecified() throws Exception {
// Given
List<Long> deviceIds = Arrays.asList(100L, 200L, 300L);
ConditionContext context = ConditionContext.builder()
.deviceIds(deviceIds)
.build();
// 未指定matchMode,默认ANY,200存在即可
JsonNode conditionValue = objectMapper.readTree("{\"deviceIds\": [200, 400]}");
// When
boolean result = strategy.evaluate(conditionValue, context);
// Then
assertTrue(result);
}
@Test
@DisplayName("多机位匹配-matchMode不区分大小写")
void shouldBeCaseInsensitiveForMatchMode() throws Exception {
// Given
List<Long> deviceIds = Arrays.asList(100L, 200L, 300L);
ConditionContext context = ConditionContext.builder()
.deviceIds(deviceIds)
.build();
// 使用小写all
JsonNode conditionValue = objectMapper.readTree("{\"deviceIds\": [100, 200], \"matchMode\": \"all\"}");
// When
boolean result = strategy.evaluate(conditionValue, context);
// Then
assertTrue(result);
}
@Test
@DisplayName("当conditionValue为null时应该返回false")
void shouldReturnFalseWhenConditionValueIsNull() {
// Given
ConditionContext context = ConditionContext.builder()
.deviceIds(Arrays.asList(100L, 200L))
.build();
// When
boolean result = strategy.evaluate(null, context);
// Then
assertFalse(result);
}
@Test
@DisplayName("当上下文中机位列表为null时应该返回false")
void shouldReturnFalseWhenContextDeviceIdsIsNull() throws Exception {
// Given
ConditionContext context = ConditionContext.builder()
.deviceIds(null)
.build();
JsonNode conditionValue = objectMapper.readTree("{\"deviceId\": 100}");
// When
boolean result = strategy.evaluate(conditionValue, context);
// Then
assertFalse(result);
}
@Test
@DisplayName("当上下文中机位列表为空时应该返回false")
void shouldReturnFalseWhenContextDeviceIdsIsEmpty() throws Exception {
// Given
ConditionContext context = ConditionContext.builder()
.deviceIds(Collections.emptyList())
.build();
JsonNode conditionValue = objectMapper.readTree("{\"deviceId\": 100}");
// When
boolean result = strategy.evaluate(conditionValue, context);
// Then
assertFalse(result);
}
@Test
@DisplayName("当deviceIds不是数组时应该返回false")
void shouldReturnFalseWhenDeviceIdsIsNotArray() throws Exception {
// Given
List<Long> deviceIds = Arrays.asList(100L, 200L);
ConditionContext context = ConditionContext.builder()
.deviceIds(deviceIds)
.build();
// deviceIds不是数组而是字符串
JsonNode conditionValue = objectMapper.readTree("{\"deviceIds\": \"100,200\"}");
// When
boolean result = strategy.evaluate(conditionValue, context);
// Then
assertFalse(result);
}
@Test
@DisplayName("当deviceIds数组为空时应该返回false")
void shouldReturnFalseWhenDeviceIdsArrayIsEmpty() throws Exception {
// Given
List<Long> deviceIds = Arrays.asList(100L, 200L);
ConditionContext context = ConditionContext.builder()
.deviceIds(deviceIds)
.build();
JsonNode conditionValue = objectMapper.readTree("{\"deviceIds\": []}");
// When
boolean result = strategy.evaluate(conditionValue, context);
// Then
assertFalse(result);
}
@Test
@DisplayName("当缺少deviceId和deviceIds字段时应该返回false")
void shouldReturnFalseWhenBothFieldsAreMissing() throws Exception {
// Given
List<Long> deviceIds = Arrays.asList(100L, 200L);
ConditionContext context = ConditionContext.builder()
.deviceIds(deviceIds)
.build();
JsonNode conditionValue = objectMapper.readTree("{\"otherField\": \"test\"}");
// When
boolean result = strategy.evaluate(conditionValue, context);
// Then
assertFalse(result);
}
@Test
@DisplayName("应该支持单个机位匹配边界情况-机位ID为0")
void shouldSupportZeroDeviceId() throws Exception {
// Given
List<Long> deviceIds = Arrays.asList(0L, 100L, 200L);
ConditionContext context = ConditionContext.builder()
.deviceIds(deviceIds)
.build();
JsonNode conditionValue = objectMapper.readTree("{\"deviceId\": 0}");
// When
boolean result = strategy.evaluate(conditionValue, context);
// Then
assertTrue(result);
}
@Test
@DisplayName("应该支持大数值机位ID")
void shouldSupportLargeDeviceId() throws Exception {
// Given
List<Long> deviceIds = Arrays.asList(999999999999L, 100L);
ConditionContext context = ConditionContext.builder()
.deviceIds(deviceIds)
.build();
JsonNode conditionValue = objectMapper.readTree("{\"deviceId\": 999999999999}");
// When
boolean result = strategy.evaluate(conditionValue, context);
// Then
assertTrue(result);
}
@Test
@DisplayName("多机位匹配-单个元素的数组应该正常工作")
void shouldWorkWithSingleElementArray() throws Exception {
// Given
List<Long> deviceIds = Arrays.asList(100L, 200L, 300L);
ConditionContext context = ConditionContext.builder()
.deviceIds(deviceIds)
.build();
JsonNode conditionValue = objectMapper.readTree("{\"deviceIds\": [200]}");
// When
boolean result = strategy.evaluate(conditionValue, context);
// Then
assertTrue(result);
}
}

View File

@@ -0,0 +1,227 @@
package com.ycwl.basic.puzzle.fill.datasource;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* 设备图片数据源策略测试
*/
@DisplayName("设备图片数据源策略测试")
@ExtendWith(MockitoExtension.class)
class DeviceImageDataSourceStrategyTest {
@Mock
private SourceMapper sourceMapper;
@InjectMocks
private DeviceImageDataSourceStrategy strategy;
private ObjectMapper objectMapper;
@BeforeEach
void setUp() {
objectMapper = new ObjectMapper();
}
@Test
@DisplayName("应该返回正确的支持类型")
void shouldReturnCorrectSupportedType() {
assertEquals("DEVICE_IMAGE", strategy.getSupportedType());
}
@Test
@DisplayName("应该返回指定设备的图片URL")
void shouldReturnDeviceImageUrl() throws Exception {
// Given
Long faceId = 123L;
Integer deviceIndex = 0;
String expectedUrl = "https://oss.example.com/device/img1.jpg";
SourceEntity source = new SourceEntity();
source.setId(1L);
source.setType(2); // 图片类型
source.setUrl(expectedUrl);
when(sourceMapper.getSourceByFaceAndDeviceIndex(faceId, deviceIndex, 2, "LATEST"))
.thenReturn(source);
DataSourceContext context = DataSourceContext.builder()
.faceId(faceId)
.build();
JsonNode sourceFilter = objectMapper.readTree("{\"type\": 2, \"deviceIndex\": 0}");
// When
String result = strategy.resolve(sourceFilter, "LATEST", context);
// Then
assertEquals(expectedUrl, result);
verify(sourceMapper, times(1)).getSourceByFaceAndDeviceIndex(faceId, deviceIndex, 2, "LATEST");
}
@Test
@DisplayName("当type为1(视频)时应该返回videoUrl")
void shouldReturnVideoUrlWhenTypeIs1() throws Exception {
// Given
Long faceId = 123L;
Integer deviceIndex = 1;
String expectedUrl = "https://oss.example.com/device/video1.mp4";
SourceEntity source = new SourceEntity();
source.setId(2L);
source.setType(1); // 视频类型
source.setVideoUrl(expectedUrl);
when(sourceMapper.getSourceByFaceAndDeviceIndex(faceId, deviceIndex, 1, "LATEST"))
.thenReturn(source);
DataSourceContext context = DataSourceContext.builder()
.faceId(faceId)
.build();
JsonNode sourceFilter = objectMapper.readTree("{\"type\": 1, \"deviceIndex\": 1}");
// When
String result = strategy.resolve(sourceFilter, "LATEST", context);
// Then
assertEquals(expectedUrl, result);
}
@Test
@DisplayName("当未指定type时应该默认使用type=2(图片)")
void shouldDefaultToImageTypeWhenTypeNotSpecified() throws Exception {
// Given
Long faceId = 123L;
Integer deviceIndex = 0;
SourceEntity source = new SourceEntity();
source.setType(2);
source.setUrl("https://oss.example.com/default.jpg");
when(sourceMapper.getSourceByFaceAndDeviceIndex(faceId, deviceIndex, 2, "LATEST"))
.thenReturn(source);
DataSourceContext context = DataSourceContext.builder()
.faceId(faceId)
.build();
JsonNode sourceFilter = objectMapper.readTree("{\"deviceIndex\": 0}");
// When
strategy.resolve(sourceFilter, "LATEST", context);
// Then
verify(sourceMapper, times(1)).getSourceByFaceAndDeviceIndex(eq(faceId), eq(deviceIndex), eq(2), anyString());
}
@Test
@DisplayName("当未指定deviceIndex时应该默认使用0")
void shouldDefaultToZeroWhenDeviceIndexNotSpecified() throws Exception {
// Given
Long faceId = 123L;
SourceEntity source = new SourceEntity();
source.setType(2);
source.setUrl("https://oss.example.com/default.jpg");
when(sourceMapper.getSourceByFaceAndDeviceIndex(faceId, 0, 2, "LATEST"))
.thenReturn(source);
DataSourceContext context = DataSourceContext.builder()
.faceId(faceId)
.build();
JsonNode sourceFilter = objectMapper.readTree("{\"type\": 2}");
// When
strategy.resolve(sourceFilter, "LATEST", context);
// Then
verify(sourceMapper, times(1)).getSourceByFaceAndDeviceIndex(eq(faceId), eq(0), anyInt(), anyString());
}
@Test
@DisplayName("当source不存在时应该返回null")
void shouldReturnNullWhenSourceNotFound() throws Exception {
// Given
Long faceId = 999L;
Integer deviceIndex = 0;
when(sourceMapper.getSourceByFaceAndDeviceIndex(anyLong(), anyInt(), anyInt(), anyString()))
.thenReturn(null);
DataSourceContext context = DataSourceContext.builder()
.faceId(faceId)
.build();
JsonNode sourceFilter = objectMapper.readTree("{\"type\": 2, \"deviceIndex\": 0}");
// When
String result = strategy.resolve(sourceFilter, "LATEST", context);
// Then
assertNull(result);
}
@Test
@DisplayName("当未指定sortStrategy时应该默认使用LATEST")
void shouldDefaultToLatestSortStrategy() throws Exception {
// Given
Long faceId = 123L;
SourceEntity source = new SourceEntity();
source.setType(2);
source.setUrl("https://oss.example.com/img.jpg");
when(sourceMapper.getSourceByFaceAndDeviceIndex(anyLong(), anyInt(), anyInt(), eq("LATEST")))
.thenReturn(source);
DataSourceContext context = DataSourceContext.builder()
.faceId(faceId)
.build();
JsonNode sourceFilter = objectMapper.readTree("{\"deviceIndex\": 0}");
// When
strategy.resolve(sourceFilter, null, context);
// Then
verify(sourceMapper, times(1)).getSourceByFaceAndDeviceIndex(anyLong(), anyInt(), anyInt(), eq("LATEST"));
}
@Test
@DisplayName("当sourceMapper抛出异常时应该返回null")
void shouldReturnNullWhenSourceMapperThrowsException() throws Exception {
// Given
Long faceId = 123L;
when(sourceMapper.getSourceByFaceAndDeviceIndex(anyLong(), anyInt(), anyInt(), anyString()))
.thenThrow(new RuntimeException("Database error"));
DataSourceContext context = DataSourceContext.builder()
.faceId(faceId)
.build();
JsonNode sourceFilter = objectMapper.readTree("{\"deviceIndex\": 0}");
// When
String result = strategy.resolve(sourceFilter, "LATEST", context);
// Then
assertNull(result);
}
}

View File

@@ -0,0 +1,158 @@
package com.ycwl.basic.puzzle.fill.datasource;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ycwl.basic.mapper.FaceMapper;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.*;
/**
* 人脸URL数据源策略测试
*/
@DisplayName("人脸URL数据源策略测试")
@ExtendWith(MockitoExtension.class)
class FaceUrlDataSourceStrategyTest {
@Mock
private FaceMapper faceMapper;
@InjectMocks
private FaceUrlDataSourceStrategy strategy;
private ObjectMapper objectMapper;
@BeforeEach
void setUp() {
objectMapper = new ObjectMapper();
}
@Test
@DisplayName("应该返回正确的支持类型")
void shouldReturnCorrectSupportedType() {
assertEquals("FACE_URL", strategy.getSupportedType());
}
@Test
@DisplayName("当face存在且有faceUrl时应该返回URL")
void shouldReturnFaceUrlWhenFaceExists() {
// Given
Long faceId = 123L;
String expectedUrl = "https://oss.example.com/face/123.jpg";
FaceEntity face = new FaceEntity();
face.setId(faceId);
face.setFaceUrl(expectedUrl);
when(faceMapper.get(faceId)).thenReturn(face);
DataSourceContext context = DataSourceContext.builder()
.faceId(faceId)
.build();
// When
String result = strategy.resolve(null, null, context);
// Then
assertEquals(expectedUrl, result);
verify(faceMapper, times(1)).get(faceId);
}
@Test
@DisplayName("当face不存在时应该返回null")
void shouldReturnNullWhenFaceNotFound() {
// Given
Long faceId = 999L;
when(faceMapper.get(faceId)).thenReturn(null);
DataSourceContext context = DataSourceContext.builder()
.faceId(faceId)
.build();
// When
String result = strategy.resolve(null, null, context);
// Then
assertNull(result);
verify(faceMapper, times(1)).get(faceId);
}
@Test
@DisplayName("当face存在但faceUrl为null时应该返回null")
void shouldReturnNullWhenFaceUrlIsNull() {
// Given
Long faceId = 123L;
FaceEntity face = new FaceEntity();
face.setId(faceId);
face.setFaceUrl(null);
when(faceMapper.get(faceId)).thenReturn(face);
DataSourceContext context = DataSourceContext.builder()
.faceId(faceId)
.build();
// When
String result = strategy.resolve(null, null, context);
// Then
assertNull(result);
}
@Test
@DisplayName("当faceMapper抛出异常时应该返回null")
void shouldReturnNullWhenFaceMapperThrowsException() {
// Given
Long faceId = 123L;
when(faceMapper.get(anyLong())).thenThrow(new RuntimeException("Database error"));
DataSourceContext context = DataSourceContext.builder()
.faceId(faceId)
.build();
// When
String result = strategy.resolve(null, null, context);
// Then
assertNull(result);
}
@Test
@DisplayName("sourceFilter和sortStrategy参数应该被忽略")
void shouldIgnoreSourceFilterAndSortStrategy() throws Exception {
// Given
Long faceId = 123L;
String expectedUrl = "https://oss.example.com/face/123.jpg";
FaceEntity face = new FaceEntity();
face.setId(faceId);
face.setFaceUrl(expectedUrl);
when(faceMapper.get(faceId)).thenReturn(face);
DataSourceContext context = DataSourceContext.builder()
.faceId(faceId)
.build();
JsonNode sourceFilter = objectMapper.readTree("{\"type\": 2}");
String sortStrategy = "LATEST";
// When
String result = strategy.resolve(sourceFilter, sortStrategy, context);
// Then
assertEquals(expectedUrl, result);
}
}

View File

@@ -0,0 +1,114 @@
package com.ycwl.basic.puzzle.fill.datasource;
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 StaticValueDataSourceStrategyTest {
private StaticValueDataSourceStrategy strategy;
private ObjectMapper objectMapper;
@BeforeEach
void setUp() {
strategy = new StaticValueDataSourceStrategy();
objectMapper = new ObjectMapper();
}
@Test
@DisplayName("应该返回正确的支持类型")
void shouldReturnCorrectSupportedType() {
assertEquals("STATIC_VALUE", strategy.getSupportedType());
}
@Test
@DisplayName("应该返回sourceFilter中的value值")
void shouldReturnValueFromSourceFilter() throws Exception {
// Given
String expectedValue = "https://oss.example.com/static/default.jpg";
JsonNode sourceFilter = objectMapper.readTree("{\"value\": \"" + expectedValue + "\"}");
DataSourceContext context = DataSourceContext.builder().build();
// When
String result = strategy.resolve(sourceFilter, null, context);
// Then
assertEquals(expectedValue, result);
}
@Test
@DisplayName("当sourceFilter为null时应该返回null")
void shouldReturnNullWhenSourceFilterIsNull() {
// Given
DataSourceContext context = DataSourceContext.builder().build();
// When
String result = strategy.resolve(null, null, context);
// Then
assertNull(result);
}
@Test
@DisplayName("当sourceFilter缺少value字段时应该返回null")
void shouldReturnNullWhenValueFieldIsMissing() throws Exception {
// Given
JsonNode sourceFilter = objectMapper.readTree("{\"otherField\": \"test\"}");
DataSourceContext context = DataSourceContext.builder().build();
// When
String result = strategy.resolve(sourceFilter, null, context);
// Then
assertNull(result);
}
@Test
@DisplayName("sortStrategy参数应该被忽略")
void shouldIgnoreSortStrategy() throws Exception {
// Given
String expectedValue = "static_value";
JsonNode sourceFilter = objectMapper.readTree("{\"value\": \"" + expectedValue + "\"}");
DataSourceContext context = DataSourceContext.builder().build();
// When
String result = strategy.resolve(sourceFilter, "LATEST", context);
// Then
assertEquals(expectedValue, result);
}
@Test
@DisplayName("应该支持各种类型的字符串值")
void shouldSupportVariousStringValues() throws Exception {
// 测试URL
testStaticValue("https://example.com/image.jpg");
// 测试普通文本
testStaticValue("Hello World");
// 测试空字符串
testStaticValue("");
// 测试包含特殊字符的文本
testStaticValue("测试中文/Special!@#$%");
}
private void testStaticValue(String value) throws Exception {
JsonNode sourceFilter = objectMapper.readTree("{\"value\": \"" + value + "\"}");
DataSourceContext context = DataSourceContext.builder().build();
String result = strategy.resolve(sourceFilter, null, context);
assertEquals(value, result);
}
}

View File

@@ -0,0 +1,306 @@
package com.ycwl.basic.puzzle.service.impl;
import com.ycwl.basic.puzzle.dto.PuzzleFillRuleDTO;
import com.ycwl.basic.puzzle.dto.PuzzleFillRuleItemDTO;
import com.ycwl.basic.puzzle.dto.PuzzleFillRuleSaveRequest;
import com.ycwl.basic.puzzle.entity.PuzzleFillRuleEntity;
import com.ycwl.basic.puzzle.entity.PuzzleFillRuleItemEntity;
import com.ycwl.basic.puzzle.mapper.PuzzleFillRuleItemMapper;
import com.ycwl.basic.puzzle.mapper.PuzzleFillRuleMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.*;
/**
* 拼图填充规则服务测试
*/
@DisplayName("拼图填充规则服务测试")
@ExtendWith(MockitoExtension.class)
class PuzzleFillRuleServiceImplTest {
@Mock
private PuzzleFillRuleMapper ruleMapper;
@Mock
private PuzzleFillRuleItemMapper itemMapper;
@InjectMocks
private PuzzleFillRuleServiceImpl service;
@BeforeEach
void setUp() {
}
@Test
@DisplayName("创建规则应该保存主规则和明细")
void shouldCreateRuleWithItems() {
// Given
PuzzleFillRuleSaveRequest request = createSaveRequest(null);
request.setItems(Arrays.asList(
createItemDTO("sfp_1", 1),
createItemDTO("sfp_2", 2)
));
// 模拟insert返回的ID
doAnswer(invocation -> {
PuzzleFillRuleEntity entity = invocation.getArgument(0);
entity.setId(100L);
return 1;
}).when(ruleMapper).insert(any(PuzzleFillRuleEntity.class));
when(itemMapper.batchInsert(anyList())).thenReturn(2);
// When
Long ruleId = service.create(request);
// Then
assertEquals(100L, ruleId);
// 验证主规则保存
verify(ruleMapper, times(1)).insert(any(PuzzleFillRuleEntity.class));
// 验证明细批量保存
ArgumentCaptor<List<PuzzleFillRuleItemEntity>> itemsCaptor = ArgumentCaptor.forClass(List.class);
verify(itemMapper, times(1)).batchInsert(itemsCaptor.capture());
List<PuzzleFillRuleItemEntity> savedItems = itemsCaptor.getValue();
assertEquals(2, savedItems.size());
assertEquals(100L, savedItems.get(0).getRuleId());
assertEquals(100L, savedItems.get(1).getRuleId());
}
@Test
@DisplayName("创建规则时如果没有明细应该只保存主规则")
void shouldCreateRuleWithoutItems() {
// Given
PuzzleFillRuleSaveRequest request = createSaveRequest(null);
request.setItems(null);
doAnswer(invocation -> {
PuzzleFillRuleEntity entity = invocation.getArgument(0);
entity.setId(100L);
return 1;
}).when(ruleMapper).insert(any(PuzzleFillRuleEntity.class));
// When
Long ruleId = service.create(request);
// Then
assertEquals(100L, ruleId);
verify(ruleMapper, times(1)).insert(any(PuzzleFillRuleEntity.class));
verify(itemMapper, never()).batchInsert(anyList());
}
@Test
@DisplayName("更新规则应该先删除旧明细再插入新明细")
void shouldUpdateRuleByDeletingOldItemsAndInsertingNew() {
// Given
PuzzleFillRuleSaveRequest request = createSaveRequest(10L);
request.setItems(Arrays.asList(
createItemDTO("sfp_1", 1),
createItemDTO("sfp_2", 2)
));
when(ruleMapper.updateById(any(PuzzleFillRuleEntity.class))).thenReturn(1);
when(itemMapper.deleteByRuleId(10L)).thenReturn(2);
when(itemMapper.batchInsert(anyList())).thenReturn(2);
// When
Boolean result = service.update(request);
// Then
assertTrue(result);
// 验证更新主规则
verify(ruleMapper, times(1)).updateById(any(PuzzleFillRuleEntity.class));
// 验证删除旧明细
verify(itemMapper, times(1)).deleteByRuleId(10L);
// 验证插入新明细
verify(itemMapper, times(1)).batchInsert(anyList());
}
@Test
@DisplayName("更新规则时ID为null应该抛出异常")
void shouldThrowExceptionWhenUpdateWithNullId() {
// Given
PuzzleFillRuleSaveRequest request = createSaveRequest(null);
// When & Then
assertThrows(IllegalArgumentException.class, () -> service.update(request));
verify(ruleMapper, never()).updateById(any());
}
@Test
@DisplayName("删除规则应该级联删除明细")
void shouldDeleteRuleWithCascade() {
// Given
Long ruleId = 10L;
when(ruleMapper.deleteById(ruleId)).thenReturn(1);
// When
Boolean result = service.delete(ruleId);
// Then
assertTrue(result);
verify(ruleMapper, times(1)).deleteById(ruleId);
// 明细由数据库外键级联删除,不需要手动删除
}
@Test
@DisplayName("查询单条规则应该包含明细")
void shouldGetRuleByIdWithItems() {
// Given
Long ruleId = 10L;
PuzzleFillRuleEntity ruleEntity = createRuleEntity(ruleId);
when(ruleMapper.selectById(ruleId)).thenReturn(ruleEntity);
List<PuzzleFillRuleItemEntity> itemEntities = Arrays.asList(
createItemEntity(1L, ruleId, "sfp_1"),
createItemEntity(2L, ruleId, "sfp_2")
);
when(itemMapper.listByRuleId(ruleId)).thenReturn(itemEntities);
// When
PuzzleFillRuleDTO dto = service.getById(ruleId);
// Then
assertNotNull(dto);
assertEquals(ruleId, dto.getId());
assertEquals("测试规则", dto.getRuleName());
assertNotNull(dto.getItems());
assertEquals(2, dto.getItems().size());
assertEquals("sfp_1", dto.getItems().get(0).getElementKey());
assertEquals("sfp_2", dto.getItems().get(1).getElementKey());
}
@Test
@DisplayName("查询单条规则时规则不存在应该返回null")
void shouldReturnNullWhenRuleNotFound() {
// Given
Long ruleId = 999L;
when(ruleMapper.selectById(ruleId)).thenReturn(null);
// When
PuzzleFillRuleDTO dto = service.getById(ruleId);
// Then
assertNull(dto);
verify(itemMapper, never()).listByRuleId(anyLong());
}
@Test
@DisplayName("查询模板的所有规则应该包含明细")
void shouldListRulesByTemplateIdWithItems() {
// Given
Long templateId = 1L;
List<PuzzleFillRuleEntity> ruleEntities = Arrays.asList(
createRuleEntity(1L),
createRuleEntity(2L)
);
when(ruleMapper.listByTemplateId(templateId)).thenReturn(ruleEntities);
when(itemMapper.listByRuleId(1L)).thenReturn(Arrays.asList(
createItemEntity(1L, 1L, "sfp_1")
));
when(itemMapper.listByRuleId(2L)).thenReturn(Arrays.asList(
createItemEntity(2L, 2L, "sfp_2")
));
// When
List<PuzzleFillRuleDTO> dtos = service.listByTemplateId(templateId);
// Then
assertEquals(2, dtos.size());
assertEquals(1L, dtos.get(0).getId());
assertEquals(1, dtos.get(0).getItems().size());
assertEquals(2L, dtos.get(1).getId());
assertEquals(1, dtos.get(1).getItems().size());
verify(itemMapper, times(1)).listByRuleId(1L);
verify(itemMapper, times(1)).listByRuleId(2L);
}
@Test
@DisplayName("启用/禁用规则应该更新状态")
void shouldToggleEnabled() {
// Given
Long ruleId = 10L;
Integer enabled = 1;
when(ruleMapper.update(any(), any())).thenReturn(1);
// When
Boolean result = service.toggleEnabled(ruleId, enabled);
// Then
assertTrue(result);
verify(ruleMapper, times(1)).update(any(), any());
}
// 辅助方法
private PuzzleFillRuleSaveRequest createSaveRequest(Long id) {
PuzzleFillRuleSaveRequest request = new PuzzleFillRuleSaveRequest();
request.setId(id);
request.setTemplateId(1L);
request.setRuleName("测试规则");
request.setConditionType("DEVICE_COUNT");
request.setConditionValue("{\"deviceCount\": 4}");
request.setPriority(100);
request.setEnabled(1);
request.setScenicId(1L);
request.setDescription("测试描述");
return request;
}
private PuzzleFillRuleItemDTO createItemDTO(String elementKey, int order) {
PuzzleFillRuleItemDTO dto = new PuzzleFillRuleItemDTO();
dto.setElementKey(elementKey);
dto.setDataSource("DEVICE_IMAGE");
dto.setSourceFilter("{\"deviceIndex\": 0}");
dto.setSortStrategy("LATEST");
dto.setItemOrder(order);
return dto;
}
private PuzzleFillRuleEntity createRuleEntity(Long id) {
PuzzleFillRuleEntity entity = new PuzzleFillRuleEntity();
entity.setId(id);
entity.setTemplateId(1L);
entity.setRuleName("测试规则");
entity.setConditionType("DEVICE_COUNT");
entity.setConditionValue("{\"deviceCount\": 4}");
entity.setPriority(100);
entity.setEnabled(1);
entity.setScenicId(1L);
return entity;
}
private PuzzleFillRuleItemEntity createItemEntity(Long id, Long ruleId, String elementKey) {
PuzzleFillRuleItemEntity entity = new PuzzleFillRuleItemEntity();
entity.setId(id);
entity.setRuleId(ruleId);
entity.setElementKey(elementKey);
entity.setDataSource("DEVICE_IMAGE");
entity.setSourceFilter("{\"deviceIndex\": 0}");
entity.setSortStrategy("LATEST");
entity.setItemOrder(id.intValue());
return entity;
}
}