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,22 @@
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 AlwaysConditionStrategy implements ConditionStrategy {
@Override
public boolean evaluate(JsonNode conditionValue, ConditionContext context) {
return true;
}
@Override
public String getSupportedType() {
return ConditionType.ALWAYS.getCode();
}
}

View File

@@ -0,0 +1,40 @@
package com.ycwl.basic.puzzle.fill.condition;
import lombok.Builder;
import lombok.Data;
import java.util.List;
/**
* 条件评估上下文
* 包含评估所需的各种运行时数据
*/
@Data
@Builder
public class ConditionContext {
/**
* 人脸ID
*/
private Long faceId;
/**
* 景区ID
*/
private Long scenicId;
/**
* 机位数量(缓存值,避免重复查询)
*/
private Integer deviceCount;
/**
* 机位ID列表(用于精确匹配指定的机位)
*/
private List<Long> deviceIds;
/**
* 可扩展的其他上下文数据
*/
private Object extra;
}

View File

@@ -0,0 +1,64 @@
package com.ycwl.basic.puzzle.fill.condition;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ycwl.basic.puzzle.entity.PuzzleFillRuleEntity;
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;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 条件评估器
* 使用策略模式,根据conditionType动态选择评估策略
*/
@Slf4j
@Component
public class ConditionEvaluator {
private final Map<String, ConditionStrategy> strategyMap;
private final ObjectMapper objectMapper;
@Autowired
public ConditionEvaluator(List<ConditionStrategy> strategies, ObjectMapper objectMapper) {
this.strategyMap = strategies.stream()
.collect(Collectors.toMap(
ConditionStrategy::getSupportedType,
Function.identity()
));
this.objectMapper = objectMapper;
log.info("初始化条件评估器,已注册{}个策略: {}", strategyMap.size(), strategyMap.keySet());
}
/**
* 评估规则条件是否匹配
*
* @param rule 规则实体
* @param context 评估上下文
* @return true-匹配, false-不匹配
*/
public boolean evaluate(PuzzleFillRuleEntity rule, ConditionContext context) {
String conditionType = rule.getConditionType();
ConditionStrategy strategy = strategyMap.get(conditionType);
if (strategy == null) {
log.warn("未找到条件类型[{}]对应的评估策略,规则ID:{}", conditionType, rule.getId());
return false;
}
try {
JsonNode conditionValue = objectMapper.readTree(rule.getConditionValue());
boolean result = strategy.evaluate(conditionValue, context);
log.debug("规则[{}]条件评估结果: {}, 条件类型: {}, 条件值: {}",
rule.getRuleName(), result, conditionType, rule.getConditionValue());
return result;
} catch (Exception e) {
log.error("规则[{}]条件评估异常,规则ID:{}", rule.getRuleName(), rule.getId(), e);
return false;
}
}
}

View File

@@ -0,0 +1,24 @@
package com.ycwl.basic.puzzle.fill.condition;
import com.fasterxml.jackson.databind.JsonNode;
/**
* 条件评估策略接口
* 使用策略模式,每种条件类型实现独立的评估逻辑,方便测试和扩展
*/
public interface ConditionStrategy {
/**
* 评估条件是否匹配
*
* @param conditionValue 条件值(JSON)
* @param context 评估上下文
* @return true-匹配, false-不匹配
*/
boolean evaluate(JsonNode conditionValue, ConditionContext context);
/**
* 获取支持的条件类型
*/
String getSupportedType();
}

View File

@@ -0,0 +1,33 @@
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 DeviceCountConditionStrategy implements ConditionStrategy {
@Override
public boolean evaluate(JsonNode conditionValue, ConditionContext context) {
if (conditionValue == null || !conditionValue.has("deviceCount")) {
return false;
}
int expectedCount = conditionValue.get("deviceCount").asInt();
Integer actualCount = context.getDeviceCount();
if (actualCount == null) {
return false;
}
return actualCount == expectedCount;
}
@Override
public String getSupportedType() {
return ConditionType.DEVICE_COUNT.getCode();
}
}

View File

@@ -0,0 +1,42 @@
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

@@ -0,0 +1,88 @@
package com.ycwl.basic.puzzle.fill.condition;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* 机位ID匹配条件策略
* 判断指定的机位ID是否在当前上下文的机位列表中
*
* 支持两种匹配模式:
* 1. 单个机位匹配: {"deviceId": 123}
* 2. 多个机位匹配: {"deviceIds": [123, 456], "matchMode": "ALL"/"ANY"}
* - ALL: 所有指定的机位都必须存在
* - ANY: 至少存在一个指定的机位(默认)
*/
@Slf4j
@Component
public class DeviceIdMatchConditionStrategy implements ConditionStrategy {
@Override
public boolean evaluate(JsonNode conditionValue, ConditionContext context) {
if (conditionValue == null) {
log.warn("DEVICE_ID_MATCH条件值为null");
return false;
}
List<Long> contextDeviceIds = context.getDeviceIds();
if (contextDeviceIds == null || contextDeviceIds.isEmpty()) {
log.debug("上下文中没有机位ID列表");
return false;
}
// 单个机位匹配模式
if (conditionValue.has("deviceId")) {
Long requiredDeviceId = conditionValue.get("deviceId").asLong();
boolean matched = contextDeviceIds.contains(requiredDeviceId);
log.debug("单机位匹配: deviceId={}, matched={}", requiredDeviceId, matched);
return matched;
}
// 多个机位匹配模式
if (conditionValue.has("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;
}
// 获取匹配模式,默认为ANY
String matchMode = conditionValue.has("matchMode")
? conditionValue.get("matchMode").asText()
: "ANY";
boolean matched;
if ("ALL".equalsIgnoreCase(matchMode)) {
// ALL模式: 所有指定的机位都必须存在
matched = contextDeviceIds.containsAll(requiredDeviceIds);
log.debug("多机位ALL匹配: requiredDeviceIds={}, matched={}", requiredDeviceIds, matched);
} else {
// ANY模式: 至少存在一个指定的机位
matched = requiredDeviceIds.stream().anyMatch(contextDeviceIds::contains);
log.debug("多机位ANY匹配: requiredDeviceIds={}, matched={}", requiredDeviceIds, matched);
}
return matched;
}
log.warn("DEVICE_ID_MATCH条件缺少deviceId或deviceIds字段");
return false;
}
@Override
public String getSupportedType() {
return "DEVICE_ID_MATCH";
}
}